日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
200行JS代碼,帶你實(shí)現(xiàn)代碼編譯器

一、前言

對(duì)于前端同學(xué)來(lái)說(shuō),編譯器可能適合神奇的魔盒,表面普通,但常常給我們驚喜。

編譯器,顧名思義,用來(lái)編譯,編譯什么呢?當(dāng)然是編譯代碼咯。

其實(shí)我們也經(jīng)常接觸到編譯器的使用場(chǎng)景:

  •  React 中 JSX 轉(zhuǎn)換成 JS 代碼;
  •  通過(guò) Babel 將 ES6 及以上規(guī)范的代碼轉(zhuǎn)換成 ES5 代碼;
  •  通過(guò)各種 Loader 將 Less / Scss 代碼轉(zhuǎn)換成瀏覽器支持的 CSS 代碼;
  •  將 TypeScript 轉(zhuǎn)換為 JavaScript 代碼。
  •  and so on...

使用場(chǎng)景非常之多,我的雙手都數(shù)不過(guò)來(lái)了。

雖然現(xiàn)在社區(qū)已經(jīng)有非常多工具能為我們完成上述工作,但了解一些編譯原理是很有必要的。接下來(lái)進(jìn)入本文主題:「200行JS代碼,帶你實(shí)現(xiàn)代碼編譯器」。

二、編譯器介紹

2.1 程序運(yùn)行方式

現(xiàn)代程序主要有兩種編譯模式:靜態(tài)編譯和動(dòng)態(tài)解釋。推薦一篇文章《Angular 2 JIT vs AOT》介紹得非常詳細(xì)。

靜態(tài)編譯

簡(jiǎn)稱 「AOT」(Ahead-Of-Time)即 「提前編譯」 ,靜態(tài)編譯的程序會(huì)在執(zhí)行前,會(huì)使用指定編譯器,將全部代碼編譯成機(jī)器碼。

在 Angular 的 AOT 編譯模式開(kāi)發(fā)流程如下:

  •  使用 TypeScript 開(kāi)發(fā) Angular 應(yīng)用
  •  運(yùn)行 ngc 編譯應(yīng)用程序
    •   使用 Angular Compiler 編譯模板,一般輸出 TypeScript 代碼
    •   運(yùn)行 tsc 編譯 TypeScript 代碼
  •  使用 Webpack 或 Gulp 等其他工具構(gòu)建項(xiàng)目,如代碼壓縮、合并等
  •  部署應(yīng)用

動(dòng)態(tài)解釋

簡(jiǎn)稱 「JIT」(Just-In-Time)即 「即時(shí)編譯」 ,動(dòng)態(tài)解釋的程序會(huì)使用指定解釋器,一邊編譯一邊執(zhí)行程序。

在 Angular 的 JIT 編譯模式開(kāi)發(fā)流程如下:

  •  使用 TypeScript 開(kāi)發(fā) Angular 應(yīng)用
  •  運(yùn)行 tsc 編譯 TypeScript 代碼
  •  使用 Webpack 或 Gulp 等其他工具構(gòu)建項(xiàng)目,如代碼壓縮、合并等
  •  部署應(yīng)用

AOT vs JIT

AOT 編譯流程:

JIT 編譯流程:

特性 AOT JIT
編譯平臺(tái) (Server) 服務(wù)器 (Browser) 瀏覽器
編譯時(shí)機(jī) Build (構(gòu)建階段) Runtime (運(yùn)行時(shí))
包大小 較小 較大
執(zhí)行性能 更好 -
啟動(dòng)時(shí)間 更短 -

除此之外 AOT 還有以下優(yōu)點(diǎn):

  •  在客戶端我們不需要導(dǎo)入體積龐大的 angular 編譯器,這樣可以減少我們 JS 腳本庫(kù)的大小。
  •  使用 AOT 編譯后的應(yīng)用,不再包含任何 HTML 片段,取而代之的是編譯生成的 TypeScript 代碼,這樣的話 TypeScript 編譯器就能提前發(fā)現(xiàn)錯(cuò)誤??偠灾?,采用 AOT 編譯模式,我們的模板是類型安全的。

2.2 現(xiàn)代編譯器工作流程

摘抄維基百科中對(duì) 編譯器[2]工作流程介紹:

        一個(gè)現(xiàn)代編譯器的主要工作流程如下:源代碼(source code)→ 預(yù)處理器(preprocessor)→ 編譯器(compiler)→ 匯編程序(assembler)→ 目標(biāo)代碼(object code)→ 鏈接器(linker)→ 可執(zhí)行文件(executables),最后打包好的文件就可以給電腦去判讀運(yùn)行了。 

這里更強(qiáng)調(diào)了編譯器的作用:「將原始程序作為輸入,翻譯產(chǎn)生目標(biāo)語(yǔ)言的等價(jià)程序」。

編譯器三個(gè)核心階段.png

目前絕大多數(shù)現(xiàn)代編譯器工作流程基本類似,包括三個(gè)核心階段:

  1.  「解析(Parsing)」 :通過(guò)詞法分析和語(yǔ)法分析,將原始代碼字符串解析成「抽象語(yǔ)法樹(shù)(Abstract Syntax Tree)」;
  2.  「轉(zhuǎn)換(Transformation)」:對(duì)抽象語(yǔ)法樹(shù)進(jìn)行轉(zhuǎn)換處理操作;
  3.  「生成代碼(Code Generation)」:將轉(zhuǎn)換之后的 AST 對(duì)象生成目標(biāo)語(yǔ)言代碼字符串。

三、編譯器實(shí)現(xiàn)

本文將通過(guò) 「The Super Tiny Compiler[3]」 源碼解讀,學(xué)習(xí)如何實(shí)現(xiàn)一個(gè)輕量編譯器,最終「實(shí)現(xiàn)將下面原始代碼字符串(Lisp 風(fēng)格的函數(shù)調(diào)用)編譯成 JavaScript 可執(zhí)行的代碼」。

  Lisp 風(fēng)格(編譯前) JavaScript 風(fēng)格(編譯后)
2 + 2 (add 2 2) add(2, 2)
4 - 2 (subtract 4 2) subtract(4, 2)
2 + (4 - 2) (add 2 (subtract 4 2)) add(2, subtract(4, 2))

話說(shuō) The Super Tiny Compiler 號(hào)稱「可能是有史以來(lái)最小的編譯器」,并且其作者 James Kyle 也是 Babel 活躍維護(hù)者之一。

讓我們開(kāi)始吧~

3.1 The Super Tiny Compiler 工作流程

現(xiàn)在對(duì)照前面編譯器的三個(gè)核心階段,了解下 The Super Tiny Compiler  編譯器核心工作流程:

圖中詳細(xì)流程如下:

  1.  執(zhí)行「入口函數(shù)」,輸入「原始代碼字符串」作為參數(shù);
 
 
 
 
  1. // 原始代碼字符串 
  2. (add 2 (subtract 42))

      2.  進(jìn)入「解析階段(Parsing)」,原始代碼字符串通過(guò)「詞法分析器(Tokenizer)」轉(zhuǎn)換為「詞法單元數(shù)組」,然后再通過(guò) 「詞法分析器(Parser)」將「詞法單元數(shù)組」轉(zhuǎn)換為「抽象語(yǔ)法樹(shù)(Abstract Syntax Tree 簡(jiǎn)稱 AST)」,并返回;

   3.   進(jìn)入「轉(zhuǎn)換階段(Transformation)」,將上一步生成的 「AST 對(duì)象」 導(dǎo)入「轉(zhuǎn)換器(Transformer)」,通過(guò)「轉(zhuǎn)換器」中的「遍歷器(Traverser)」,將代碼轉(zhuǎn)換為我們所需的「新的 AST 對(duì)象」;

    4.   進(jìn)入「代碼生成階段(Code Generation)」,將上一步返回的「新 AST 對(duì)象」通過(guò)「代碼生成器(CodeGenerator)」,轉(zhuǎn)換成 「JavaScript Code」;

    5.   「代碼編譯結(jié)束」,返回 「JavaScript Code」。

上述流程看完后可能一臉懵逼,不過(guò)沒(méi)事,請(qǐng)保持頭腦清醒,先有個(gè)整個(gè)流程的印象,接下來(lái)我們開(kāi)始閱讀代碼:

3.2 入口方法

首先定義一個(gè)入口方法 compiler ,接收原始代碼字符串作為參數(shù),返回最終 JavaScript Code:

 
 
 
 
  1. // 編譯器入口方法 參數(shù):原始代碼字符串 input 
  2. function compiler(input) { 
  3.   let tokens = tokenizer(input); 
  4.   let ast    = parser(tokens); 
  5.   let newAst = transformer(ast); 
  6.   let output = codeGenerator(newAst); 
  7.   return output; 
  8. }

3.3 解析階段

在解析階段中,我們定義「詞法分析器方法」 tokenizer  和「語(yǔ)法分析器方法」 parser 然后分別實(shí)現(xiàn):

 
 
 
 
  1. // 詞法分析器 參數(shù):原始代碼字符串 input 
  2. function tokenizer(input) {}; 
  3. // 語(yǔ)法分析器 參數(shù):詞法單元數(shù)組tokens 
  4. function parser(tokens) {};

詞法分析器

「詞法分析器方法」 tokenizer 的主要任務(wù):遍歷整個(gè)原始代碼字符串,將原始代碼字符串轉(zhuǎn)換為「詞法單元數(shù)組(tokens)」,并返回。

在遍歷過(guò)程中,匹配每種字符并處理成「詞法單元」壓入「詞法單元數(shù)組」,如當(dāng)匹配到左括號(hào)( ( )時(shí),將往「詞法單元數(shù)組(tokens)「壓入一個(gè)」詞法單元對(duì)象」({type: 'paren', value:'('})。

 
 
 
 
  1. // 詞法分析器 參數(shù):原始代碼字符串 input 
  2. function tokenizer(input) { 
  3.   let current = 0;  // 當(dāng)前解析的字符索引,作為游標(biāo) 
  4.   let tokens = [];  // 初始化詞法單元數(shù)組 
  5.   // 循環(huán)遍歷原始代碼字符串,讀取詞法單元數(shù)組 
  6.   while (current < input.length) { 
  7.     let char = input[current]; 
  8.     // 匹配左括號(hào),匹配成功則壓入對(duì)象 {type: 'paren', value:'('} 
  9.     if (char === '(') { 
  10.       tokens.push({ 
  11.         type: 'paren', 
  12.         value: '(' 
  13.       }); 
  14.       current++; 
  15.       continue; // 自增current,完成本次循環(huán),進(jìn)入下一個(gè)循環(huán) 
  16.     } 
  17.     // 匹配右括號(hào),匹配成功則壓入對(duì)象 {type: 'paren', value:')'} 
  18.     if (char === ')') { 
  19.       tokens.push({ 
  20.         type: 'paren', 
  21.         value: ')' 
  22.       }); 
  23.       current++; 
  24.       continue; 
  25.     }  
  26.     // 匹配空白字符,匹配成功則跳過(guò) 
  27.     // 使用 \s 匹配,包括空格、制表符、換頁(yè)符、換行符、垂直制表符等 
  28.     let WHITESPACE = /\s/; 
  29.     if (WHITESPACE.test(char)) { 
  30.       current++; 
  31.       continue;
  32.      } 
  33.     // 匹配數(shù)字字符,使用 [0-9]:匹配 
  34.     // 匹配成功則壓入{type: 'number', value: value} 
  35.     // 如 (add 123 456) 中 123 和 456 為兩個(gè)數(shù)值詞法單元 
  36.     let NUMBERS = /[0-9]/; 
  37.     if (NUMBERS.test(char)) { 
  38.       let value = ''; 
  39.       // 匹配連續(xù)數(shù)字,作為數(shù)值
  40.        while (NUMBERS.test(char)) { 
  41.         value += char; 
  42.         char = input[++current]; 
  43.       } 
  44.       tokens.push({ type: 'number', value }); 
  45.       continue; 
  46.     } 
  47.     // 匹配形雙引號(hào)包圍的字符串 
  48.     // 匹配成功則壓入 { type: 'string', value: value } 
  49.     // 如 (concat "foo" "bar") 中 "foo" 和 "bar" 為兩個(gè)字符串詞法單元 
  50.     if (char === '"') { 
  51.       let value = ''; 
  52.       char = input[++current]; // 跳過(guò)左雙引號(hào) 
  53.       // 獲取兩個(gè)雙引號(hào)之間所有字符 
  54.       while (char !== '"') { 
  55.         value += char; 
  56.         char = input[++current]; 
  57.       } 
  58.       char = input[++current];// 跳過(guò)右雙引號(hào) 
  59.       tokens.push({ type: 'string', value }); 
  60.       continue; 
  61.     } 
  62.     // 匹配函數(shù)名,要求只含大小寫(xiě)字母,使用 [a-z] 匹配 i 模式 
  63.     // 匹配成功則壓入 { type: 'name', value: value } 
  64.     // 如 (add 2 4) 中 add 為一個(gè)名稱詞法單元 
  65.     let LETTERS = /[a-z]/i; 
  66.     if (LETTERS.test(char)) { 
  67.       let value = ''; 
  68.       // 獲取連續(xù)字符 
  69.       while (LETTERS.test(char)) { 
  70.         value += char; 
  71.         char = input[++current]; 
  72.       } 
  73.       tokens.push({ type: 'name', value }); 
  74.       continue; 
  75.     } 
  76.     // 當(dāng)遇到無(wú)法識(shí)別的字符,拋出錯(cuò)誤提示,并退出 
  77.     thrownewTypeError('I dont know what this character is: ' + char); 
  78.   } 
  79.   // 詞法分析器的最后返回詞法單元數(shù)組 
  80.   return tokens; 
  81. }

語(yǔ)法分析器

「語(yǔ)法分析器方法」 parser 的主要任務(wù):將「詞法分析器」返回的「詞法單元數(shù)組」,轉(zhuǎn)換為能夠描述語(yǔ)法成分及其關(guān)系的中間形式(「抽象語(yǔ)法樹(shù) AST」)。

 
 
 
 
  1. // 語(yǔ)法分析器 參數(shù):詞法單元數(shù)組tokens 
  2. function parser(tokens) { 
  3.   let current = 0; // 設(shè)置當(dāng)前解析的詞法單元的索引,作為游標(biāo) 
  4.   // 遞歸遍歷(因?yàn)楹瘮?shù)調(diào)用允許嵌套),將詞法單元轉(zhuǎn)成 LISP 的 AST 節(jié)點(diǎn) 
  5.   function walk() { 
  6.     // 獲取當(dāng)前索引下的詞法單元 token 
  7.     let token = tokens[current]; 
  8.     // 數(shù)值類型詞法單元 
  9.     if (token.type === 'number') { 
  10.       current++; // 自增當(dāng)前 current 值 
  11.       // 生成一個(gè) AST節(jié)點(diǎn) 'NumberLiteral',表示數(shù)值字面量 
  12.       return { 
  13.         type: 'NumberLiteral', 
  14.         value: token.value, 
  15.       }; 
  16.     } 
  17.     // 字符串類型詞法單元 
  18.     if (token.type === 'string') { 
  19.       current++; 
  20.       // 生成一個(gè) AST節(jié)點(diǎn) 'StringLiteral',表示字符串字面量 
  21.       return { 
  22.         type: 'StringLiteral', 
  23.         value: token.value, 
  24.       }; 
  25.     } 
  26.     // 函數(shù)類型詞法單元 
  27.     if (token.type === 'paren' && token.value === '(') { 
  28.       // 跳過(guò)左括號(hào),獲取下一個(gè)詞法單元作為函數(shù)名 
  29.       token = tokens[++current]; 
  30.       let node = { 
  31.         type: 'CallExpression', 
  32.         name: token.value, 
  33.         params: [] 
  34.       };
  35.       // 再次自增 current 變量,獲取參數(shù)詞法單元 
  36.       token = tokens[++current]; 
  37.       // 遍歷每個(gè)詞法單元,獲取函數(shù)參數(shù),直到出現(xiàn)右括號(hào)")" 
  38.       while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) { 
  39.         node.params.push(walk()); 
  40.         token = tokens[current]; 
  41.       } 
  42.       current++; // 跳過(guò)右括號(hào) 
  43.       return node; 
  44.     } 
  45.     // 無(wú)法識(shí)別的字符,拋出錯(cuò)誤提示 
  46.     thrownewTypeError(token.type); 
  47.   } 
  48.   // 初始化 AST 根節(jié)點(diǎn) 
  49.   let ast = { 
  50.     type: 'Program', 
  51.     body: [], 
  52.   }; 
  53.   // 循環(huán)填充 ast.body 
  54.   while (current < tokens.length) { 
  55.     ast.body.push(walk()); 
  56.   } 
  57.   // 最后返回ast 
  58.   return ast; 
  59. }

3.4 轉(zhuǎn)換階段

在轉(zhuǎn)換階段中,定義了轉(zhuǎn)換器 transformer 函數(shù),使用詞法分析器返回的 LISP 的 AST 對(duì)象作為參數(shù),將 AST 對(duì)象轉(zhuǎn)換成一個(gè)新的 AST 對(duì)象。

為了方便代碼組織,我們定義一個(gè)遍歷器 traverser 方法,用來(lái)處理每一個(gè)節(jié)點(diǎn)的操作。

 
 
 
 
  1. // 遍歷器 參數(shù):ast 和 visitor 
  2. function traverser(ast, visitor) { 
  3.   // 定義方法 traverseArray 
  4.   // 用于遍歷 AST節(jié)點(diǎn)數(shù)組,對(duì)數(shù)組中每個(gè)元素調(diào)用 traverseNode 方法。 
  5.   function traverseArray(array, parent) { 
  6.     array.forEach(child => { 
  7.       traverseNode(child, parent); 
  8.     }); 
  9.   } 
  10.   // 定義方法 traverseNode 
  11.   // 用于處理每個(gè) AST 節(jié)點(diǎn),接受一個(gè) node 和它的父節(jié)點(diǎn) parent 作為參數(shù) 
  12.   function traverseNode(node, parent) { 
  13.     // 獲取 visitor 上對(duì)應(yīng)方法的對(duì)象 
  14.     let methods = visitor[node.type]; 
  15.     // 獲取 visitor 的 enter 方法,處理操作當(dāng)前 node 
  16.     if (methods && methods.enter) { 
  17.       methods.enter(node, parent); 
  18.     } 
  19.     switch (node.type) { 
  20.       // 根節(jié)點(diǎn) 
  21.       case'Program': 
  22.         traverseArray(node.body, node); 
  23.         break;
  24.       // 函數(shù)調(diào)用 
  25.       case'CallExpression': 
  26.         traverseArray(node.params, node); 
  27.         break; 
  28.       // 數(shù)值和字符串,忽略 
  29.       case'NumberLiteral': 
  30.       case'StringLiteral': 
  31.         break; 
  32.       // 當(dāng)遇到無(wú)法識(shí)別的字符,拋出錯(cuò)誤提示,并退出 
  33.       default: 
  34.         thrownewTypeError(node.type); 
  35.     } 
  36.     if (methods && methods.exit) { 
  37.       methods.exit(node, parent); 
  38.     } 
  39.   } 
  40.   // 首次執(zhí)行,開(kāi)始遍歷 
  41.   traverseNode(ast, null); 
  42. }

在看「遍歷器」 traverser 方法時(shí),建議結(jié)合下面介紹的「轉(zhuǎn)換器」 transformer 方法閱讀:

 
 
 
 
  1. // 轉(zhuǎn)化器,參數(shù):ast 
  2. function transformer(ast) { 
  3.   // 創(chuàng)建 newAST,與之前 AST 類似,Program:作為新 AST 的根節(jié)點(diǎn) 
  4.   let newAst = { 
  5.     type: 'Program', 
  6.     body: [], 
  7.   }; 
  8.   // 通過(guò) _context 維護(hù)新舊 AST,注意 _context 是一個(gè)引用,從舊的 AST 到新的 AST。 
  9.   ast._context = newAst.body; 
  10.   // 通過(guò)遍歷器遍歷 處理舊的 AST 
  11.   traverser(ast, { 
  12.     // 數(shù)值,直接原樣插入新AST,類型名稱 NumberLiteral 
  13.     NumberLiteral: { 
  14.       enter(node, parent) { 
  15.         parent._context.push({ 
  16.           type: 'NumberLiteral', 
  17.           value: node.value, 
  18.         }); 
  19.       }, 
  20.     }, 
  21.     // 字符串,直接原樣插入新AST,類型名稱 StringLiteral 
  22.     StringLiteral: { 
  23.       enter(node, parent) { 
  24.         parent._context.push({ 
  25.           type: 'StringLiteral', 
  26.           value: node.value, 
  27.         }); 
  28.       }, 
  29.     }, 
  30.     // 函數(shù)調(diào)用 
  31.     CallExpression: { 
  32.       enter(node, parent) { 
  33.         // 創(chuàng)建不同的AST節(jié)點(diǎn) 
  34.         let expression = { 
  35.           type: 'CallExpression', 
  36.           callee: { 
  37.             type: 'Identifier', 
  38.             name: node.name, 
  39.           }, 
  40.           arguments: [], 
  41.         }; 
  42.         // 函數(shù)調(diào)用有子類,建立節(jié)點(diǎn)對(duì)應(yīng)關(guān)系,供子節(jié)點(diǎn)使用 
  43.         node._context = expression.arguments; 
  44.         // 頂層函數(shù)調(diào)用算是語(yǔ)句,包裝成特殊的AST節(jié)點(diǎn) 
  45.         if (parent.type !== 'CallExpression') { 
  46.           expression = { 
  47.             type: 'ExpressionStatement', 
  48.             expression: expression, 
  49.           }; 
  50.         } 
  51.         parent._context.push(expression); 
  52.       }, 
  53.     } 
  54.   }); 
  55.   return newAst; 
  56. }

重要一點(diǎn),這里通過(guò) _context 引用來(lái)「維護(hù)新舊 AST 對(duì)象」,管理方便,避免污染舊 AST 對(duì)象。

3.5 代碼生成

接下來(lái)到了最后一步,我們定義「代碼生成器」 codeGenerator 方法,通過(guò)遞歸,將新的 AST 對(duì)象代碼轉(zhuǎn)換成 JavaScript 可執(zhí)行代碼字符串。

 
 
 
 
  1. // 代碼生成器 參數(shù):新 AST 對(duì)象 
  2. function codeGenerator(node) { 
  3.   switch (node.type) { 
  4.     // 遍歷 body 屬性中的節(jié)點(diǎn),且遞歸調(diào)用 codeGenerator,按行輸出結(jié)果 
  5.     case'Program': 
  6.       return node.body.map(codeGenerator) 
  7.         .join('\n'); 
  8.     // 表達(dá)式,處理表達(dá)式內(nèi)容,并用分號(hào)結(jié)尾 
  9.     case'ExpressionStatement': 
  10.       return ( 
  11.         codeGenerator(node.expression) + 
  12.         ';' 
  13.       ); 
  14.     // 函數(shù)調(diào)用,添加左右括號(hào),參數(shù)用逗號(hào)隔開(kāi) 
  15.     case'CallExpression': 
  16.       return ( 
  17.         codeGenerator(node.callee) + 
  18.         '(' + 
  19.         node.arguments.map(codeGenerator) 
  20.           .join(', ') + 
  21.         ')' 
  22.       ); 
  23.     // 標(biāo)識(shí)符,返回其 name 
  24.     case'Identifier': 
  25.       return node.name; 
  26.     // 數(shù)值,返回其 value 
  27.     case'NumberLiteral': 
  28.       return node.value; 
  29.     // 字符串,用雙引號(hào)包裹再輸出 
  30.     case'StringLiteral': 
  31.       return'"' + node.value + '"'; 
  32.     // 當(dāng)遇到無(wú)法識(shí)別的字符,拋出錯(cuò)誤提示,并退出 
  33.     default: 
  34.       thrownewTypeError(node.type); 
  35.   } 
  36. }

3.6 編譯器測(cè)試

截止上一步,我們完成簡(jiǎn)易編譯器的代碼開(kāi)發(fā)。接下來(lái)通過(guò)前面原始需求的代碼,測(cè)試編譯器效果如何:

 
 
 
 
  1. const add = (a, b) => a + b; 
  2. const subtract = (a, b) => a - b; 
  3. const source = "(add 2 (subtract 4 2))"; 
  4. const target = compiler(source); // "add(2, (subtract(4, 2));" 
  5. const result = eval(target); // Ok result is 4

3.7 工作流程小結(jié)

總結(jié) The Super Tiny Compiler 編譯器整個(gè)工作流程:

「1、input => tokenizer => tokens」

「2、tokens => parser => ast」

「3、ast => transformer => newAst」

「4、newAst => generator => output」

其實(shí)多數(shù)編譯器的工作流程都大致相同:

四、手寫(xiě) Webpack 編譯器

根據(jù)之前介紹的 The Super Tiny Compiler編譯器核心工作流程,再來(lái)手寫(xiě) Webpack 的編譯器,會(huì)讓你有種眾享絲滑的感覺(jué)~

話說(shuō),有些面試官喜歡問(wèn)這個(gè)呢。當(dāng)然,手寫(xiě)一遍能讓我們更了解 Webpack 的構(gòu)建流程,這個(gè)章節(jié)我們簡(jiǎn)要介紹一下。

4.1 Webpack 構(gòu)建流程分析

從啟動(dòng)構(gòu)建到輸出結(jié)果一系列過(guò)程:

1.  「初始化參數(shù)」

解析 Webpack 配置參數(shù),合并 Shell 傳入和 webpack.config.js 文件配置的參數(shù),形成最后的配置結(jié)果。

2.  「開(kāi)始編譯」

上一步得到的參數(shù)初始化 compiler 對(duì)象,注冊(cè)所有配置的插件,插件監(jiān)聽(tīng) Webpack 構(gòu)建生命周期的事件節(jié)點(diǎn),做出相應(yīng)的反應(yīng),執(zhí)行對(duì)象的 run 方法開(kāi)始執(zhí)行編譯。

3.  「確定入口」

從配置的 entry 入口,開(kāi)始解析文件構(gòu)建 AST 語(yǔ)法樹(shù),找出依賴,遞歸下去。

4.  「編譯模塊」

遞歸中根據(jù)「文件類型」和 「loader 配置」,調(diào)用所有配置的 loader 對(duì)文件進(jìn)行轉(zhuǎn)換,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過(guò)了本步驟的處理。

5  「完成模塊編譯并輸出」

遞歸完事后,得到每個(gè)文件結(jié)果,包含每個(gè)模塊以及他們之間的依賴關(guān)系,根據(jù) entry 配置生成代碼塊 chunk 。

 6. 「輸出完成」

輸出所有的 chunk 到文件系統(tǒng)。

注意:在構(gòu)建生命周期中有一系列插件在做合適的時(shí)機(jī)做合適事情,比如 UglifyPlugin 會(huì)在 loader 轉(zhuǎn)換遞歸完對(duì)結(jié)果使用 UglifyJs 壓縮「覆蓋之前的結(jié)果」。

4.2 代碼實(shí)現(xiàn)

手寫(xiě) Webpack 需要實(shí)現(xiàn)以下三個(gè)核心方法:

  •  createAssets : 收集和處理文件的代碼;
  •  createGraph :根據(jù)入口文件,返回所有文件依賴圖;
  •  bundle : 根據(jù)依賴圖整個(gè)代碼并輸出;

1. createAssets

 
 
 
 
  1. function createAssets(filename){ 
  2.     const content = fs.readFileSync(filename, "utf-8"); // 根據(jù)文件名讀取文件內(nèi)容 
  3.     // 將讀取到的代碼內(nèi)容,轉(zhuǎn)換為 AST 
  4.     const ast = parser.parse(content, { 
  5.         sourceType: "module"http:// 指定源碼類型 
  6.     }) 
  7.     const dependencies = []; // 用于收集文件依賴的路徑 
  8.     // 通過(guò) traverse 提供的操作 AST 的方法,獲取每個(gè)節(jié)點(diǎn)的依賴路徑 
  9.     traverse(ast, { 
  10.         ImportDeclaration: ({node}) => { 
  11.             dependencies.push(node.source.value); 
  12.         } 
  13.     }); 
  14.     // 通過(guò) AST 將 ES6 代碼轉(zhuǎn)換成 ES5 代碼 
  15.     const { code } = babel.transformFromAstSync(ast, null, { 
  16.         presets: ["@babel/preset-env"] 
  17.     }); 
  18.     let id = moduleId++; 
  19.     return { 
  20.         id, 
  21.         filename, 
  22.         code, 
  23.         dependencies 
  24.     } 
  25. }

2. createGraph

 
 
 
 
  1. function createGraph(entry) { 
  2.     const mainAsset = createAssets(entry); // 獲取入口文件下的內(nèi)容 
  3.     const queue = [mainAsset]; 
  4.     for(const asset of queue){ 
  5.         const dirname = path.dirname(asset.filename); 
  6.         asset.mapping = {}; 
  7.         asset.dependencies.forEach(relativePath => { 
  8.             const absolutePath = path.join(dirname, relativePath); // 轉(zhuǎn)換文件路徑為絕對(duì)路徑 
  9.             const child = createAssets(absolutePath); 
  10.             asset.mapping[relativePath] = child.id; 
  11.             queue.push(child); // 遞歸去遍歷所有子節(jié)點(diǎn)的文件 
  12.         }) 
  13.     } 
  14.     return queue; 
  15. }

3. bunlde

 
 
 
 
  1. function bundle(graph) { 
  2.     let modules = ""; 
  3.     graph.forEach(item => { 
  4.         modules += ` 
  5.             ${item.id}: [ 
  6.                 function (require, module, exports){ 
  7.                     ${item.code} 
  8.                 }, 
  9.                 ${JSON.stringify(item.mapping)} 
  10.             ], 
  11.         ` 
  12.     }) 
  13.     return` 
  14.         (function(modules){ 
  15.             function require(id){ 
  16.                 const [fn, mapping] = modules[id]; 
  17.                 function localRequire(relativePath){ 
  18.                     return require(mapping[relativePath]); 
  19.                 } 
  20.                 const module = { 
  21.                     exports: {} 
  22.                 } 
  23.                 fn(localRequire, module, module.exports); 
  24.                 return module.exports; 
  25.             } 
  26.             require(0); 
  27.         })({${modules}}) 
  28.     ` 
  29. }

五、總結(jié)

本文從編譯器概念和基本工作流程開(kāi)始介紹,然后通過(guò) The Super Tiny Compiler 譯器源碼,詳細(xì)介紹核心工作流程實(shí)現(xiàn),包括「詞法分析器」、「語(yǔ)法分析器」、「遍歷器」和「轉(zhuǎn)換器」的基本實(shí)現(xiàn),最后通過(guò)「代碼生成器」,將各個(gè)階段代碼結(jié)合起來(lái),實(shí)現(xiàn)了這個(gè)號(hào)稱「可能是有史以來(lái)最小的編譯器。」

本文也簡(jiǎn)要介紹了「手寫(xiě) Webpack 的實(shí)現(xiàn)」,需要讀者自行完善和深入喲!

「是不是覺(jué)得很神奇~」

當(dāng)然通過(guò)本文學(xué)習(xí),也僅僅是編譯器相關(guān)知識(shí)的邊山一腳,要學(xué)的知識(shí)還有非常多,不過(guò)好的開(kāi)頭,更能促進(jìn)我們學(xué)習(xí)動(dòng)力。加油!

最后,文中介紹到的代碼,我存放在 Github 上:

  1.  [learning]the-super-tiny-compiler.js[4]
  2.  [writing]webpack-compiler.js[5]

新聞標(biāo)題:200行JS代碼,帶你實(shí)現(xiàn)代碼編譯器
當(dāng)前URL:http://m.5511xx.com/article/djpcdss.html