<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          1W字帶你用 200 行 JS 代碼實(shí)現(xiàn)一個(gè)編譯器

          共 15028字,需瀏覽 31分鐘

           ·

          2020-09-12 12:40


          ?

          最近看到掘金、前端公眾號(hào)好多 ES2020 的文章,想說一句:放開我,我還學(xué)得動(dòng)!

          ?

          先問大家一句,日常項(xiàng)目開發(fā)中你能離開 ES6 嗎?

          一、前言

          對(duì)于前端同學(xué)來說,編譯器可能適合神奇的魔盒?,表面普通,但常常給我們驚喜。
          編譯器,顧名思義,用來編譯,編譯什么呢?當(dāng)然是編譯代碼咯?。

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

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


          使用場景非常之多,我的雙手都數(shù)不過來了。?
          雖然現(xiàn)在社區(qū)已經(jīng)有非常多工具能為我們完成上述工作,但了解一些編譯原理是很有必要的。接下來進(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)編譯

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

          (圖片來自:https://segmentfault.com/a/1190000008739157)

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

          • 使用 TypeScript 開發(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)解釋

          簡稱 「JIT」(Just-In-Time)即 「即時(shí)編譯」 ,動(dòng)態(tài)解釋的程序會(huì)使用指定解釋器,一邊編譯一邊執(zhí)行程序。
          (圖片來自:https://segmentfault.com/a/1190000008739157[1]

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

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

          AOT vs JIT

          AOT 編譯流程:(圖片來自:https://segmentfault.com/a/1190000008739157)

          JIT 編譯流程:(圖片來自:https://segmentfault.com/a/1190000008739157)

          特性AOTJIT
          編譯平臺(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 腳本庫的大小。
          • 使用 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)語言的等價(jià)程序」。

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

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

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

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

          本文將通過 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))


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

          讓我們開始吧~

          3.1 The Super Tiny Compiler 工作流程

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

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

          1. 執(zhí)行「入口函數(shù)」,輸入「原始代碼字符串」作為參數(shù);
          // 原始代碼字符串
          (add 2 (subtract 4 2))
          1. 進(jìn)入「解析階段(Parsing)」,原始代碼字符串通過「詞法分析器(Tokenizer)轉(zhuǎn)換為詞法單元數(shù)組,然后再通過 「詞法分析器(Parser)詞法單元數(shù)組」轉(zhuǎn)換為抽象語法樹(Abstract Syntax Tree 簡稱 AST)」,并返回;

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

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

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


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

          3.2 入口方法

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

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

          3.3 解析階段

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

          // 詞法分析器 參數(shù):原始代碼字符串 input
          function tokenizer(input) {};

          // 語法分析器 參數(shù):詞法單元數(shù)組tokens
          function parser(tokens) {};

          詞法分析器

          「詞法分析器方法」 tokenizer 的主要任務(wù):遍歷整個(gè)原始代碼字符串,將原始代碼字符串轉(zhuǎn)換為「詞法單元數(shù)組(tokens)」,并返回。
          在遍歷過程中,匹配每種字符并處理成「詞法單元」壓入「詞法單元數(shù)組」,如當(dāng)匹配到左括號(hào)( ( )時(shí),將往「詞法單元數(shù)組(tokens)「壓入一個(gè)」詞法單元對(duì)象」{type: 'paren', value:'('})。

          // 詞法分析器 參數(shù):原始代碼字符串 input
          function tokenizer(input) {
          let current = 0; // 當(dāng)前解析的字符索引,作為游標(biāo)
          let tokens = []; // 初始化詞法單元數(shù)組
          // 循環(huán)遍歷原始代碼字符串,讀取詞法單元數(shù)組
          while (current < input.length) {
          let char = input[current];
          // 匹配左括號(hào),匹配成功則壓入對(duì)象 {type: 'paren', value:'('}
          if (char === '(') {
          tokens.push({
          type: 'paren',
          value: '('
          });
          current++;
          continue; // 自增current,完成本次循環(huán),進(jìn)入下一個(gè)循環(huán)
          }
          // 匹配右括號(hào),匹配成功則壓入對(duì)象 {type: 'paren', value:')'}
          if (char === ')') {
          tokens.push({
          type: 'paren',
          value: ')'
          });
          current++;
          continue;
          }

          // 匹配空白字符,匹配成功則跳過
          // 使用 \s 匹配,包括空格、制表符、換頁符、換行符、垂直制表符等
          let WHITESPACE = /\s/;
          if (WHITESPACE.test(char)) {
          current++;
          continue;
          }
          // 匹配數(shù)字字符,使用 [0-9]:匹配
          // 匹配成功則壓入{type: 'number', value: value}
          // 如 (add 123 456) 中 123 和 456 為兩個(gè)數(shù)值詞法單元
          let NUMBERS = /[0-9]/;
          if (NUMBERS.test(char)) {
          let value = '';
          // 匹配連續(xù)數(shù)字,作為數(shù)值
          while (NUMBERS.test(char)) {
          value += char;
          char = input[++current];
          }
          tokens.push({ type: 'number', value });
          continue;
          }
          // 匹配形雙引號(hào)包圍的字符串
          // 匹配成功則壓入 { type: 'string', value: value }
          // 如 (concat "foo" "bar") 中 "foo" 和 "bar" 為兩個(gè)字符串詞法單元
          if (char === '"') {
          let value = '';
          char = input[++current]; // 跳過左雙引號(hào)
          // 獲取兩個(gè)雙引號(hào)之間所有字符
          while (char !== '"') {
          value += char;
          char = input[++current];
          }
          char = input[++current];// 跳過右雙引號(hào)
          tokens.push({ type: 'string', value });
          continue;
          }
          // 匹配函數(shù)名,要求只含大小寫字母,使用 [a-z] 匹配 i 模式
          // 匹配成功則壓入 { type: 'name', value: value }
          // 如 (add 2 4) 中 add 為一個(gè)名稱詞法單元
          let LETTERS = /[a-z]/i;
          if (LETTERS.test(char)) {
          let value = '';
          // 獲取連續(xù)字符
          while (LETTERS.test(char)) {
          value += char;
          char = input[++current];
          }
          tokens.push({ type: 'name', value });
          continue;
          }
          // 當(dāng)遇到無法識(shí)別的字符,拋出錯(cuò)誤提示,并退出
          throw new TypeError('I dont know what this character is: ' + char);
          }
          // 詞法分析器的最后返回詞法單元數(shù)組
          return tokens;
          }

          語法分析器

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

          // 語法分析器 參數(shù):詞法單元數(shù)組tokens
          function parser(tokens) {
          let current = 0; // 設(shè)置當(dāng)前解析的詞法單元的索引,作為游標(biāo)
          // 遞歸遍歷(因?yàn)楹瘮?shù)調(diào)用允許嵌套),將詞法單元轉(zhuǎn)成 LISP 的 AST 節(jié)點(diǎn)
          function walk() {
          // 獲取當(dāng)前索引下的詞法單元 token
          let token = tokens[current];

          // 數(shù)值類型詞法單元
          if (token.type === 'number') {
          current++; // 自增當(dāng)前 current 值
          // 生成一個(gè) AST節(jié)點(diǎn) 'NumberLiteral',表示數(shù)值字面量
          return {
          type: 'NumberLiteral',
          value: token.value,
          };
          }

          // 字符串類型詞法單元
          if (token.type === 'string') {
          current++;
          // 生成一個(gè) AST節(jié)點(diǎn) 'StringLiteral',表示字符串字面量
          return {
          type: 'StringLiteral',
          value: token.value,
          };
          }

          // 函數(shù)類型詞法單元
          if (token.type === 'paren' && token.value === '(') {
          // 跳過左括號(hào),獲取下一個(gè)詞法單元作為函數(shù)名
          token = tokens[++current];

          let node = {
          type: 'CallExpression',
          name: token.value,
          params: []
          };

          // 再次自增 current 變量,獲取參數(shù)詞法單元
          token = tokens[++current];

          // 遍歷每個(gè)詞法單元,獲取函數(shù)參數(shù),直到出現(xiàn)右括號(hào)")"
          while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) {
          node.params.push(walk());
          token = tokens[current];
          }

          current++; // 跳過右括號(hào)
          return node;
          }
          // 無法識(shí)別的字符,拋出錯(cuò)誤提示
          throw new TypeError(token.type);
          }

          // 初始化 AST 根節(jié)點(diǎn)
          let ast = {
          type: 'Program',
          body: [],
          };

          // 循環(huán)填充 ast.body
          while (current < tokens.length) {
          ast.body.push(walk());
          }

          // 最后返回ast
          return ast;
          }


          3.4 轉(zhuǎn)換階段

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

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

          // 遍歷器 參數(shù):ast 和 visitor
          function traverser(ast, visitor) {
          // 定義方法 traverseArray
          // 用于遍歷 AST節(jié)點(diǎn)數(shù)組,對(duì)數(shù)組中每個(gè)元素調(diào)用 traverseNode 方法。
          function traverseArray(array, parent) {
          array.forEach(child => {
          traverseNode(child, parent);
          });
          }

          // 定義方法 traverseNode
          // 用于處理每個(gè) AST 節(jié)點(diǎn),接受一個(gè) node 和它的父節(jié)點(diǎn) parent 作為參數(shù)
          function traverseNode(node, parent) {
          // 獲取 visitor 上對(duì)應(yīng)方法的對(duì)象
          let methods = visitor[node.type];
          // 獲取 visitor 的 enter 方法,處理操作當(dāng)前 node
          if (methods && methods.enter) {
          methods.enter(node, parent);
          }

          switch (node.type) {
          // 根節(jié)點(diǎn)
          case 'Program':
          traverseArray(node.body, node);
          break;
          // 函數(shù)調(diào)用
          case 'CallExpression':
          traverseArray(node.params, node);
          break;
          // 數(shù)值和字符串,忽略
          case 'NumberLiteral':
          case 'StringLiteral':
          break;

          // 當(dāng)遇到無法識(shí)別的字符,拋出錯(cuò)誤提示,并退出
          default:
          throw new TypeError(node.type);
          }
          if (methods && methods.exit) {
          methods.exit(node, parent);
          }
          }
          // 首次執(zhí)行,開始遍歷
          traverseNode(ast, null);
          }


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

          // 轉(zhuǎn)化器,參數(shù):ast
          function transformer(ast) {
          // 創(chuàng)建 newAST,與之前 AST 類似,Program:作為新 AST 的根節(jié)點(diǎn)
          let newAst = {
          type: 'Program',
          body: [],
          };

          // 通過 _context 維護(hù)新舊 AST,注意 _context 是一個(gè)引用,從舊的 AST 到新的 AST。
          ast._context = newAst.body;

          // 通過遍歷器遍歷 處理舊的 AST
          traverser(ast, {
          // 數(shù)值,直接原樣插入新AST,類型名稱 NumberLiteral
          NumberLiteral: {
          enter(node, parent) {
          parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
          });
          },
          },
          // 字符串,直接原樣插入新AST,類型名稱 StringLiteral
          StringLiteral: {
          enter(node, parent) {
          parent._context.push({
          type: 'StringLiteral',
          value: node.value,
          });
          },
          },
          // 函數(shù)調(diào)用
          CallExpression: {
          enter(node, parent) {
          // 創(chuàng)建不同的AST節(jié)點(diǎn)
          let expression = {
          type: 'CallExpression',
          callee: {
          type: 'Identifier',
          name: node.name,
          },
          arguments: [],
          };

          // 函數(shù)調(diào)用有子類,建立節(jié)點(diǎn)對(duì)應(yīng)關(guān)系,供子節(jié)點(diǎn)使用
          node._context = expression.arguments;

          // 頂層函數(shù)調(diào)用算是語句,包裝成特殊的AST節(jié)點(diǎn)
          if (parent.type !== 'CallExpression') {

          expression = {
          type: 'ExpressionStatement',
          expression: expression,
          };
          }
          parent._context.push(expression);
          },
          }
          });
          return newAst;
          }


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


          3.5 代碼生成

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

          // 代碼生成器 參數(shù):新 AST 對(duì)象
          function codeGenerator(node) {

          switch (node.type) {
          // 遍歷 body 屬性中的節(jié)點(diǎn),且遞歸調(diào)用 codeGenerator,按行輸出結(jié)果
          case 'Program':
          return node.body.map(codeGenerator)
          .join('\n');

          // 表達(dá)式,處理表達(dá)式內(nèi)容,并用分號(hào)結(jié)尾
          case 'ExpressionStatement':
          return (
          codeGenerator(node.expression) +
          ';'
          );

          // 函數(shù)調(diào)用,添加左右括號(hào),參數(shù)用逗號(hào)隔開
          case 'CallExpression':
          return (
          codeGenerator(node.callee) +
          '(' +
          node.arguments.map(codeGenerator)
          .join(', ') +
          ')'
          );

          // 標(biāo)識(shí)符,返回其 name
          case 'Identifier':
          return node.name;
          // 數(shù)值,返回其 value
          case 'NumberLiteral':
          return node.value;

          // 字符串,用雙引號(hào)包裹再輸出
          case 'StringLiteral':
          return '"' + node.value + '"';

          // 當(dāng)遇到無法識(shí)別的字符,拋出錯(cuò)誤提示,并退出
          default:
          throw new TypeError(node.type);
          }
          }


          3.6 編譯器測試

          截止上一步,我們完成簡易編譯器的代碼開發(fā)。接下來通過前面原始需求的代碼,測試編譯器效果如何:

          const add = (a, b) => a + b;
          const subtract = (a, b) => a - b;
          const source = "(add 2 (subtract 4 2))";
          const target = compiler(source); // "add(2, (subtract(4, 2));"

          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ù)編譯器的工作流程都大致相同:

          四、手寫 Webpack 編譯器

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


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

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

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

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

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

          1. 「開始編譯」

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

          1. 「確定入口」

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

          1. 「編譯模塊」

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

          1. 「完成模塊編譯并輸出」

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

          1. 「輸出完成」

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

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


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

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

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

          1. createAssets

          function createAssets(filename){
          const content = fs.readFileSync(filename, "utf-8"); // 根據(jù)文件名讀取文件內(nèi)容

          // 將讀取到的代碼內(nèi)容,轉(zhuǎn)換為 AST
          const ast = parser.parse(content, {
          sourceType: "module" // 指定源碼類型
          })
          const dependencies = []; // 用于收集文件依賴的路徑

          // 通過 traverse 提供的操作 AST 的方法,獲取每個(gè)節(jié)點(diǎn)的依賴路徑
          traverse(ast, {
          ImportDeclaration: ({node}) => {
          dependencies.push(node.source.value);
          }
          });

          // 通過 AST 將 ES6 代碼轉(zhuǎn)換成 ES5 代碼
          const { code } = babel.transformFromAstSync(ast, null, {
          presets: ["@babel/preset-env"]
          });

          let id = moduleId++;
          return {
          id,
          filename,
          code,
          dependencies
          }
          }

          2. createGraph

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

          3. bunlde

          function bundle(graph) {
          let modules = "";
          graph.forEach(item => {
          modules += `
          ${item.id}: [
          function (require, module, exports){
          ${item.code}
          },
          ${JSON.stringify(item.mapping)}
          ],
          `

          })
          return `
          (function(modules){
          function require(id){
          const [fn, mapping] = modules[id];
          function localRequire(relativePath){
          return require(mapping[relativePath]);
          }

          const module = {
          exports: {}
          }

          fn(localRequire, module, module.exports);

          return module.exports;
          }
          require(0);
          })({${modules}})
          `

          }


          五、總結(jié)

          本文從編譯器概念和基本工作流程開始介紹,然后通過 The Super Tiny Compiler 譯器源碼,詳細(xì)介紹核心工作流程實(shí)現(xiàn),包括「詞法分析器」、「語法分析器」、「遍歷器」「轉(zhuǎn)換器」的基本實(shí)現(xiàn),最后通過「代碼生成器」,將各個(gè)階段代碼結(jié)合起來,實(shí)現(xiàn)了這個(gè)號(hào)稱「可能是有史以來最小的編譯器?!?/strong>
          本文也簡要介紹了「手寫 Webpack 的實(shí)現(xiàn)」,需要讀者自行完善和深入喲!
          「是不是覺得很神奇~」

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

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

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

          六、參考資料

          1. 《The Super Tiny Compiler》[6]
          2. 《有史以來最小的編譯器源碼解析》[7]
          3. 《Angular 2 JIT vs AOT》[8]

          Reference

          [1]

          https://segmentfault.com/a/1190000008739157: https://segmentfault.com/a/1190000008739157

          [2]

          編譯器: https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8?wprov=srpw1_0

          [3]

          The Super Tiny Compiler: https://the-super-tiny-compiler.glitch.me/

          [4]

          [learning]the-super-tiny-compiler.js: https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Frontend/learningSourceCode/%5Blearning%5Dthe-super-tiny-compiler.js

          [5]

          [writing]webpack-compiler.js: https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Frontend/learningSourceCode/%5Bwriting%5Dwebpack-compiler.js

          [6]

          《The Super Tiny Compiler》: https://the-super-tiny-compiler.glitch.me/

          [7]

          《有史以來最小的編譯器源碼解析》: https://segmentfault.com/a/1190000016402699

          [8]

          《Angular 2 JIT vs AOT》: https://segmentfault.com/a/1190000008739157



          END


          如果你喜歡探討技術(shù),或者對(duì)本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當(dāng)然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。魚頭的微信號(hào)是:krisChans95 也可以掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。


          瀏覽 39
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99视频亚洲 | 伊人大香蕉视频网站 | 99热99精品 | 免费A片视频 | 国产成人精品午夜精品 |