1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        由 Babel 理解前端編譯原理

        共 13509字,需瀏覽 28分鐘

         ·

        2022-04-08 21:59

        術(shù)??堅(jiān)??

        背景

        我們知道編程語言主要分為「編譯型語言」和「解釋型語言」,編譯型語言是在代碼運(yùn)行前編譯器將編程語言轉(zhuǎn)換成機(jī)器語言,運(yùn)行時(shí)不需要重新翻譯,直接使用編譯的結(jié)果就行了。而解釋型語言也是需要將編程語言轉(zhuǎn)換成機(jī)器語言,但是是在運(yùn)行時(shí)轉(zhuǎn)換的。

        通常我們都將 JavaScript 歸類為「解釋型語言」,以至于很多人都誤以為前端代碼是不需要編譯的,但其實(shí) JavaScript 引擎進(jìn)行編譯的步驟和傳統(tǒng)的編譯語言非常相似,只不過與傳統(tǒng)的編譯語言不同,它不是提前編譯的。并且隨著現(xiàn)代瀏覽器和前端領(lǐng)域的蓬勃發(fā)展,編譯器在前端領(lǐng)域的應(yīng)用越來越廣泛,就日常工作而言,包括但不限于以下幾個(gè)方面:

        • v8 引擎、typescript 編譯器(tsc)
        • webpack loader 編譯器(acorn[19]),babel、SWC 等編譯工具。
        • angular、Vue 等框架的模板編譯器、jsx

        作為前端開發(fā),我們沒必要對(duì)這些編譯器或者底層的編譯原理了如指掌,但是如果能對(duì)編譯原理有一些基本的認(rèn)識(shí),也能夠?qū)窈蟮娜粘i_發(fā)很有幫助。本文就帶領(lǐng)大家學(xué)習(xí)下編譯原理的一些基本概念,并以 Babel 為例講解下前端編譯的基本流程。

        概述

        我們先來回顧下編譯原理的基本知識(shí),從宏觀上來說,編譯本質(zhì)上是一種轉(zhuǎn)換技術(shù),從一門編程語言轉(zhuǎn)換成另一門編程語言,或者從高級(jí)語言轉(zhuǎn)換成低級(jí)語言,或者從高級(jí)語言到高級(jí)語言,所謂的高級(jí)語言和低級(jí)語言主要是指下面的區(qū)分:

        • 高級(jí)語言:有很多用于描述邏輯的語言特性,比如分支、循環(huán)、函數(shù)、面向?qū)ο蟮?,接近人的思維,可以讓開發(fā)者快速的通過它來表達(dá)各種邏輯。比如 c++、javascript。
        • 低級(jí)語言:與硬件和執(zhí)行細(xì)節(jié)有關(guān),會(huì)操作寄存器、內(nèi)存,具體做內(nèi)存與寄存器之間的復(fù)制,需要開發(fā)者理解熟悉計(jì)算機(jī)的工作原理,熟悉具體的執(zhí)行細(xì)節(jié)。比如匯編語言、機(jī)器語言。

        無論是怎樣的編譯過程,基本都會(huì)是下面的一個(gè)過程:

        上面的約定的編譯規(guī)則,就是指各種編程語言的語法規(guī)則,不同的編譯器會(huì)產(chǎn)出不同的“編譯結(jié)果”,例如 C/C++ 語言經(jīng)過編譯得到二進(jìn)制的機(jī)器碼,然后交給操作系統(tǒng),例如當(dāng)我們運(yùn)行 tsc 命令就會(huì)將 TS 代碼編譯為 js 代碼,再比如執(zhí)行 babel 命令會(huì)將 es6+ 的代碼編譯為指定目標(biāo)(es5)的 js 代碼。

        一般來說,整個(gè)編譯過程主要分為兩個(gè)階段:編譯 前端編譯后端,大致分為下面的幾個(gè)過程:

        從上圖可以看到,編譯前端主要就是幫助計(jì)算機(jī)閱讀源代碼并理解源代碼的結(jié)構(gòu)、含義、作用等,將源代碼由一串無意義的字符流解析為一個(gè)個(gè)的有特定含義的構(gòu)件。通常情況下,編譯前端會(huì)產(chǎn)生一種用于給編譯后端消費(fèi)的中間產(chǎn)物,比如我們常見的抽象語法樹 AST,而編譯后端則是在前端解析的結(jié)果和基礎(chǔ)上,進(jìn)一步優(yōu)化和轉(zhuǎn)換并生成最終的目標(biāo)代碼。

        上下文無關(guān)文法

        前面提到編譯器會(huì)根據(jù)「約定的編譯規(guī)則」進(jìn)行編譯,這里「約定的編譯規(guī)則」就是指「上下文無關(guān)文法(CFG)[20]」。CFG 用于在理論上的形式化定義一門語言的語法,或者說,用于系統(tǒng)地描述程序設(shè)計(jì)語言的構(gòu)造(比如表達(dá)式和語句)。

        實(shí)際上,幾乎所有程序設(shè)計(jì)語言都是通過上下文無關(guān)文法來定義的,與正則表達(dá)式比較像,但是比正則表達(dá)式功能更強(qiáng)大,它能表達(dá)非常復(fù)雜的文法,比如 C 語言語法用正則表達(dá)式來表示不可能做到,但是可以用 CFG 的一組規(guī)則來表達(dá)。

        要理解上下文無關(guān)文法,需要先理解下面幾個(gè)概念:

        • 終結(jié)符:可以理解為基礎(chǔ)符號(hào),詞法符號(hào),是不可替代的,是固定存在的,不能通過文法規(guī)則生成的。
        • 非終結(jié)符:句法變量,是可以替代的
        • 產(chǎn)生式規(guī)則:語法是由終結(jié)符集、非終結(jié)符集和產(chǎn)生式規(guī)則共同組成。產(chǎn)生式規(guī)則定義了符號(hào)之間如何轉(zhuǎn)換替代。規(guī)則的左側(cè)是規(guī)則頭,即非終結(jié)符,是可以被替代的符號(hào);右側(cè)是產(chǎn)生式,是具體的內(nèi)容。

        例如下面 a, b, c, d 為終結(jié)符(用小寫表示),(S, A) 為非終結(jié)符(用大寫表示)。S -> cAd, A -> a | ab 表示產(chǎn)生式規(guī)則。S->cAd,然后可以產(chǎn)生"cad",“cabd” 等符合文法的內(nèi)容

        S?->?cAd
        A?->?a?|?ab

        上下文無關(guān)文法比較抽象,不是這里學(xué)習(xí)的重點(diǎn),感興趣的話可以專門深入了解下,這里知乎上也有篇回答可以參考下 應(yīng)該如何理解「上下文無關(guān)文法」?[21]

        下面我們來簡(jiǎn)單模擬下如何用 CFG 來定義一門語言的語法, 我們假設(shè)一個(gè)極其簡(jiǎn)單的語言,這個(gè)語言只能像 js 那樣聲明整數(shù)型常量,以及聲明不接受任何參數(shù)且只能直接返回常量加法的箭頭函數(shù)。

        const?a?=?10
        const?b?=?20
        const?c?=?()?=>??a?+?b

        這個(gè)語言的文法表達(dá)如下:

        program?::?statement+
        statement?::?declare?|?func
        declare?::?CONST?VARIABLE?ASSIGN?INTEGER
        func?::?CONST?LPAREN?RPAREN?ARROW?expression
        expression?::?VARIABLE?+?VARIABLE


        CONST??::?"const"
        ASSIGN?::?"="
        LPAREN?::?"("
        RPAREN?::?")"
        ARROW??::?"=>"
        INTEGER?::?\d+
        VARIABLE?::?\w[\w\d$_]*

        可以看出,整個(gè)文法的表達(dá),涵蓋了很多正則表達(dá)式的概念。該表達(dá)是一種自頂向下的規(guī)范:

        1. 首先入口約束了程序(program)是由一條(及以上)的表達(dá)式(statement)構(gòu)成,
        1. 表達(dá)式又可以由聲明語句(declare)或函數(shù)語句(func)構(gòu)成。
        1. 聲明語句依次由 const、關(guān)鍵字、符號(hào) =、整數(shù) 從左到右排列構(gòu)成。
        1. 整數(shù)的定義則直接使用正則表達(dá)式來約束。函數(shù)語句也是類似。

        大家可以觀察到,上述的文法分成了上下兩個(gè)大的部分。上半部分定義了語句以及由語句遞歸構(gòu)造的表達(dá),通常稱為「語法規(guī)則」(grammar rules);下半部分定義了可通過排列構(gòu)成語句的基本詞匯,通常稱為「詞法規(guī)則」(lexer rules)。


        在實(shí)踐中,詞法規(guī)則往往沒有單獨(dú)羅列,而是直接寫入到語法規(guī)則中。比如上述文法可簡(jiǎn)化為:

        program?::?statement+
        statement?::?declare?|?func
        declare?::?"const"?variable?"="?integer
        func?::?"const"?variable?"="?"("?")"?"=>"?expression
        expression?::?variable?+?variable
        variable?::?\w[\w\d$_]*
        integer??::?\d+

        上面的文法表達(dá)形式叫做 BNF[22],它是用來描述上下文無關(guān)文法的一種描述語言,形式為<符號(hào)> ::= <使用符號(hào)的表達(dá)式>,這里的 <符號(hào)> 是非終結(jié)符,而表達(dá)式由一個(gè)符號(hào)序列,或用指示選擇的豎杠 '|' 分隔的多個(gè)符號(hào)序列構(gòu)成,每個(gè)符號(hào)序列整體都是左端的符號(hào)的一種可能的替代。從未在左端出現(xiàn)的符號(hào)就是終結(jié)符。

        有了 BNF,我們就可以實(shí)現(xiàn)語言文法的具體化、公式化,甚至可以自己實(shí)現(xiàn)一個(gè)語言來解決特定領(lǐng)域的問題。再來看個(gè)例子,用 BNF 來描述四則運(yùn)算:

        result?::=?number?("+"|"-")?exp?|?number?//?非終結(jié)符
        exp?::=?number("*"|"/")?exp?|?number?//?非終結(jié)符
        number?::=?[0-9]+?//?終結(jié)符

        另外,我們也可以窺探下 ECMA 和 JSON 的 BNF:

        • JSON Schema 的 BNF:Syntax - JSON Schema[23]
        • ECMA 的 BNF:function&class bnf[24]

        編譯器工作流程

        接下來我們就相對(duì)深入的來看一下編譯的各個(gè)階段。

        詞法分析

        就像我們學(xué)習(xí)一門語言的第一步是學(xué)習(xí)單詞一樣,編譯器識(shí)別源代碼的第一步就是要要進(jìn)行分詞,識(shí)別出每一個(gè)單詞或符號(hào)。這個(gè)階段,詞法分析器會(huì)將源代碼拆分成一組 token 串:

        1. 首先,通過對(duì)源代碼的字符串從左到右進(jìn)行掃描,以空白字符(空格、換行、制表符等)為分隔符,拆分為一個(gè)個(gè)無類型的 token 。
        1. 其次,再根據(jù)詞法規(guī)則,利用「有限狀態(tài)機(jī)[25]」對(duì)第一步拆分的 Token 進(jìn)行字符串模式匹配,以識(shí)別每一個(gè) Token 的類型(v8 token.h[26])。

        一般而言,token 是一個(gè)有類型和值的數(shù)據(jù)結(jié)構(gòu),而 token 流簡(jiǎn)單理解可以是 token 數(shù)組。以下面一行代碼為例:

        const?name?=?'xujianglong';

        //?根據(jù)?js?的語法規(guī)則,?大致會(huì)生成如下的?token?流
        [
        ??{?type:?"CONST",?value:?"const"?},
        ??{?type:?"IDENTIFIER",?value:?"name"?},
        ??{?type:?"ASSIGN",?value:?"="?},
        ??{?type:?"STRING",?value:?"xujianglong"?},
        ??{?type:?"SEMICOLON",?value:?";"?},
        ]

        那么有限狀態(tài)機(jī)是個(gè)什么概念呢?它是怎么把字符串代碼轉(zhuǎn)化為 token 的呢?

        首先我們想一下,詞法描述的是最小的單詞格式,比如上面例子的那一行代碼為例,利用空白字符拆分成這幾個(gè) token:['const','name','=','xujianglong',';'],但是怎么去識(shí)別每種 token 的類型呢,最簡(jiǎn)單粗暴的方式我們可以寫個(gè) if else語句或者寫個(gè)正則,但是這樣貌似不太優(yōu)雅且不容易維護(hù),而使用狀態(tài)機(jī)是許多編程語言都使用的方式。

        有限狀態(tài)機(jī)(英語:finite-state machine,縮寫:FSM)又稱有限狀態(tài)自動(dòng)機(jī)(英語:finite-state automation,縮寫:FSA),簡(jiǎn)稱狀態(tài)機(jī),是表示有限個(gè)狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動(dòng)作等行為的數(shù)學(xué)計(jì)算模型。

        如圖所示,用戶從其他狀態(tài)機(jī)進(jìn)入 S1 狀態(tài)機(jī),如果用戶輸入 1,則繼續(xù)進(jìn)入 S1 狀態(tài)機(jī),如果用戶輸入了 0,則進(jìn)入下一個(gè)狀態(tài)機(jī) S2。在 S2 狀態(tài)機(jī)中,如果用戶繼續(xù)輸入 1,則繼續(xù)進(jìn)入 S2 狀態(tài)機(jī),如果用戶輸入了 0,則回到 S1 狀態(tài)機(jī)。這是一個(gè)循環(huán)的過程。

        聽起來有點(diǎn)抽象,對(duì)比到代碼分詞中來說,我們可以把每個(gè)單詞的處理過程當(dāng)成一種狀態(tài),將整體的輸入(源代碼)按照每個(gè)字符依次去讀取,根據(jù)每次讀取到的字符來更改當(dāng)前的狀態(tài),每個(gè) token 識(shí)別完了就可以拋出來。我們舉個(gè)簡(jiǎn)單的四則運(yùn)算的例子:10 + 20 - 30

        首先我們定義了三種狀態(tài)機(jī),分別是 NUMBER 代表數(shù)值,ADD 代表加號(hào),SUB 代表減號(hào):

        1. 當(dāng)分析到 "1" 時(shí),因?yàn)楸敬屋斎胛覀冃枰淖儬顟B(tài)機(jī)內(nèi)部狀態(tài)為 NUMBER,繼續(xù)迭代下一個(gè)字符 “0”,此時(shí)因?yàn)?"1" 和 "0" 是一個(gè)整體可以不被分開的。
        1. 當(dāng)分析到 "+" 時(shí),狀態(tài)機(jī)中輸入為 “+”, 顯然 “+” 是一個(gè)運(yùn)算符號(hào),它并不能和上一次的 “10” 拼接在一起。所以此時(shí)狀態(tài)改變,我們將上一次的 currentToken 也就是 "10" 推入 tokens 中,同時(shí)改變狀態(tài)機(jī)狀態(tài)為 ADD。
        1. 依次類推,最終會(huì)輸出如下 tokens 數(shù)組:
        [
        ??{?type:?"NUMBER",?value:?"10"?},
        ??{?type:?"ADD",?value:?"+"?},
        ??{?type:?"NUMBER",?value:?"20"?},
        ??{?type:?"SUB",?value:?"-"?},
        ??{?type:?"NUMBER",?value:?"30"?},
        ]

        語法分析

        在這個(gè)階段,語法分析器(parser)會(huì)將詞法分析中得到的 token 數(shù)組轉(zhuǎn)化為 抽象語法樹 AST 。比如前面定義變量的那行代碼可以在這個(gè)在線工具 AST explorer[27] 中查看生成的 AST:

        {
        ??"type":?"Program",
        ??"start":?0,
        ??"end":?28,
        ??"body":?[
        ????{
        ??????"type":?"VariableDeclaration",
        ??????"start":?1,
        ??????"end":?28,
        ??????"declarations":?[
        ????????{
        ??????????"type":?"VariableDeclarator",
        ??????????"start":?7,
        ??????????"end":?27,
        ??????????"id":?{
        ????????????"type":?"Identifier",
        ????????????"start":?7,
        ????????????"end":?11,
        ????????????"name":?"name"
        ??????????},
        ??????????"init":?{
        ????????????"type":?"Literal",
        ????????????"start":?14,
        ????????????"end":?27,
        ????????????"value":?"xujianglong",
        ????????????"raw":?"'xujianglong'"
        ??????????}
        ????????}
        ??????],
        ??????"kind":?"const"
        ????}
        ??],
        ??"sourceType":?"module"
        }

        對(duì)于 JavaScript 語言來說,AST 也有一套約定的規(guī)范:GitHub - estree/estree: The ESTree Spec[28],社區(qū)稱之為 estree,借助這個(gè)規(guī)范,整個(gè)前端社區(qū)的一些工具便可以產(chǎn)出一套統(tǒng)一的數(shù)據(jù)格式而無需關(guān)心下游,下游的消費(fèi)類工具統(tǒng)一使用這個(gè)統(tǒng)一的格式進(jìn)行處理而無需關(guān)心上游,這樣就做到了上下游的解耦。以 webpack 為例,其底層是 acorn[29] 工具,acorn 會(huì)把 js 源碼轉(zhuǎn)化為上述的標(biāo)準(zhǔn) estree,而 webpack 作為下游便可以消費(fèi)該 estree,比如遍歷,提取和分析 require/import 依賴,轉(zhuǎn)換代碼并輸出。

        生成 AST 的過程需要遵循語法規(guī)則(用上下文無關(guān)文法表示),在上面代碼中,我們用到了「VariableDeclaration」,其語法規(guī)則可以表示為:

        VariableDeclaration?::?Kind?Identifier?Init?;
        Kind?::?Const?|?Let?|?Var;
        Init?::?'='?Expression?|?Identifier?|?Literal;
        Expression?::?BinaryExpression?|?ConditionalExpression?|?...;
        Literal?::?StringLiteral?|?...;

        有了語法規(guī)則之后,我們就需要思考編譯器是如何將 token 流,在語法規(guī)則的約束下,轉(zhuǎn)換成 AST 的呢?生成 AST 也主要是兩大方向:

        1. 一是將文法規(guī)則的約束硬編碼到編譯器的代碼邏輯中,這種是特定語言的編譯器使用的常見方案,這種方案往往是人工編寫 parse 代碼,對(duì)輸入源碼的各種錯(cuò)誤和異??梢愿?xì)致地報(bào)告和處理。比如前面提到的 arorn,以及 tsc,babel,以及熟悉的 vue,angular 的模板編譯器等,都主要是這種方法。
        1. 二是使用自動(dòng)生成工具將文法規(guī)則直接轉(zhuǎn)換成語法 parse 代碼。這種更常用于非特定的編程語言,比如一些業(yè)務(wù)中自定義的簡(jiǎn)單但易變的語法,或僅僅只是字符串文本的復(fù)雜處理規(guī)則。

        我們這里以第一種方式最基礎(chǔ)的 「遞歸下降算法」(遞歸下降的編譯技術(shù),是業(yè)界最常使用的手寫編譯器實(shí)現(xiàn)編譯前端的技術(shù)之一,感興趣可以專門去深入研究下)為例,簡(jiǎn)單描述示例代碼生成 AST 的過程:

        嘗試匹配?VariableDeclaration

        匹配到?Const

        匹配到?Identifier

        嘗試匹配?Init,遞歸下降

        匹配到?'='

        嘗試匹配?Expression,遞歸下降

        匹配失敗,回溯

        匹配到?Literal,回溯

        VariableDeclaration?匹配成功,構(gòu)造相應(yīng)類型節(jié)點(diǎn)插入?AST

        語義分析

        并不是所有的編譯器都有語義分析,比如 Babel 就沒有。不過對(duì)于其它大部分編程語言(包括 TypeScript)的編譯器來說,是有語義分析這一步驟的,特別是靜態(tài)類型語言,類型檢查就屬于語義分析的其中一個(gè)步驟

        語義分析階段,編譯器開始對(duì) AST 進(jìn)行一次或多次的遍歷,檢查程序的語義規(guī)則。主要包括聲明檢查和類型檢查,如上一個(gè)賦值語句中,就需要檢查:

        • 語句中的變量 name 是否被聲明過
        • const 類型變量是否被改變
        • 加號(hào)運(yùn)算的兩個(gè)操作數(shù)的類型是否匹配
        • 函數(shù)的參數(shù)數(shù)量和類型是否與其聲明的參數(shù)數(shù)量及類型匹配

        語義檢查的步驟和人對(duì)源代碼的閱讀和理解的步驟差不多,一般都是在遍歷 AST 的過程中,遇到變量聲明和函數(shù)聲明時(shí),則將變量名--類型、函數(shù)名--返回類型--參數(shù)數(shù)量及類型等信息保存到符號(hào)表里,當(dāng)遇到使用變量和函數(shù)的地方,則根據(jù)名稱在符號(hào)表中查找和檢查,查找該名稱是否被聲明過,該名稱的類型是否被正確的使用等等。

        語義檢查時(shí),也會(huì)對(duì)語法樹進(jìn)行一些優(yōu)化,比如將只含常量的表達(dá)式先計(jì)算出來,如:

        a?=?1?+?2?*?9;

        會(huì)被優(yōu)化成:

        a?=?19;

        語義分析完成后,源代碼的結(jié)構(gòu)解析就已經(jīng)完成了,所有編譯期錯(cuò)誤都已被排除,所有使用到的變量名和函數(shù)名都綁定到其聲明位置(地址)了,至此編譯器可以說是真正理解了源代碼,可以開始進(jìn)行代碼生成和代碼優(yōu)化了。

        中間代碼生成與優(yōu)化

        一般的編譯器并不直接生成目標(biāo)代碼,而是先生成某種中間代碼,然后再生成目標(biāo)代碼。之所以先生成中間代碼,主要有以下幾個(gè)原因:

        1. 為了降低編譯器開發(fā)的難度,將高級(jí)語言翻譯成中間代碼、將此中間代碼再翻譯成目標(biāo)代碼的難度都比直接將高級(jí)語言翻譯成目標(biāo)代碼的難度要低。
        1. 為了增加編譯器的模塊化、可移植性和可擴(kuò)展性,一般來說,中間代碼既獨(dú)立于任何高級(jí)語言,也獨(dú)立于任何目標(biāo)機(jī)器架構(gòu),這就為開發(fā)出適應(yīng)性廣泛的編譯器提供了媒介
        1. 為了代碼優(yōu)化,一般來說,計(jì)算機(jī)直接生成的代碼比人手寫的匯編要龐大、重復(fù)很多,計(jì)算機(jī)科學(xué)家們對(duì)一些具有固定格式的中間代碼的進(jìn)行大量的研究工作,提出了很多廣泛應(yīng)用的、效率非常高的優(yōu)化算法,可以對(duì)中間代碼進(jìn)行優(yōu)化,比直接對(duì)目標(biāo)代碼進(jìn)行優(yōu)化的效果要好很多。

        JavaScript 的編譯器 v8 引擎早期是沒有中間代碼生成的,直接從 AST 生成本地可執(zhí)行的代碼,但由于缺少了轉(zhuǎn)換為字節(jié)碼這一中間過程,也就減少了優(yōu)化代碼的機(jī)會(huì)。為了提高性能, v8 開始采用了引入字節(jié)碼的架構(gòu),先把 AST 編譯為字節(jié)碼,再通過 JIT 工具轉(zhuǎn)換成本地代碼。

        v8 引擎的編譯過程這里不做過多介紹,后續(xù)可作為單獨(dú)的分享。

        • V8 引擎詳解(三)——從字節(jié)碼看 V8 的演變[30]
        • 理解 V8 的字節(jié)碼「譯」[31]

        生成目標(biāo)代碼

        有了中間代碼后,目標(biāo)代碼的生成是相對(duì)容易的,因?yàn)橹虚g代碼在設(shè)計(jì)的時(shí)候就考慮到要能輕松生成目標(biāo)代碼。不同的高級(jí)語言的編譯器生成的目標(biāo)代碼不一樣,如:

        • C、C++、Go,目標(biāo)代碼都是匯編語言(可能目標(biāo)代碼不一樣),在經(jīng)過匯編器最終得到機(jī)器碼;
        • Java 經(jīng)過 javac 編譯后,生成字節(jié)碼,只經(jīng)過了編譯的詞法分析、語法分析、語義分析和中間代碼生成,運(yùn)行時(shí)再由解釋器逐條將字節(jié)碼解釋為機(jī)器碼來執(zhí)行;
        • JavaScript,在運(yùn)行時(shí)經(jīng)過整個(gè)編譯流程,不過也是先生成字節(jié)碼,然后解析器解析字節(jié)碼執(zhí)行;
        • 至于 webpack、babel 這些前端工具則是最終編譯成相應(yīng)的 js 代碼。

        Babel 的編譯流程

        前面我們提到,一般編譯器是指高級(jí)語言到低級(jí)語言的轉(zhuǎn)換工具,特殊地像前端的一些工具類型的轉(zhuǎn)換,如 ts 轉(zhuǎn) js,js 轉(zhuǎn) js 等這些都是高級(jí)語言到高級(jí)語言的轉(zhuǎn)換工具,通常被叫做轉(zhuǎn)換編譯器,簡(jiǎn)稱轉(zhuǎn)譯器 (Transpiler),所以 Babel 就是是一個(gè) JavaScript 編譯器。

        Babel 主要用于將采用 ECMAScript 2015+ 語法編寫的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,并且還可以把目標(biāo)環(huán)境不支持的 api 進(jìn)行 polyfill。以便能夠運(yùn)行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中。例如下面的例子:

        // Babel 輸入:ES2015 箭頭函數(shù)
        [1,?2,?3].map(n?=>?n?+?1);

        // Babel 輸出:ES5 語法實(shí)現(xiàn)的同等功能
        [1,?2,?3].map(function(n)?{
        ??return?n?+?1;
        });

        Babel 的工作流程可分為如下幾個(gè)步驟:

        下面詳細(xì)介紹下 babel 工作流程的各個(gè)階段。

        parse(解析)

        parse 這個(gè)階段將原始代碼字符串轉(zhuǎn)為 AST 樹,parse 廣義上來說包括了我們前面編譯流程中講到的 詞法分析、語法分析這兩個(gè)階段。parse 過程中會(huì)有一些 babel 插件,讓 babel 可以解析出更多的語法,比如 jsx。

        parse(sourceCode)?=>?AST

        parse 階段,主要通過 @babel/parser這個(gè)包進(jìn)行轉(zhuǎn)換,之前叫 babylon,是基于 acorn 實(shí)現(xiàn)的,擴(kuò)展了很多語法,可以支持 esnext(現(xiàn)在支持到 es2020)、jsx、flow、typescript 等語法的解析,其中 jsx、flow、typescript 這些非標(biāo)準(zhǔn)的語法的解析需要指定語法插件。

        我們可以手動(dòng)調(diào)用下 parser 的方法進(jìn)行轉(zhuǎn)換,便會(huì)得到一份 AST:

        const?parser?=?require("@babel/parser");
        const?fs?=?require('fs');

        const?code?=?`function?square(n)?{
        ??return?n?*?n;};`
        const?result?=?parser.parse(code)
        console.log(result);

        最終輸出的 AST 如下所示:

        {
        ??type:?"FunctionDeclaration",
        ??id:?{
        ????type:?"Identifier",
        ????name:?"square"
        ??},
        ??params:?[{
        ????type:?"Identifier",
        ????name:?"n"
        ??}],
        ??body:?{
        ????type:?"BlockStatement",
        ????body:?[{
        ??????type:?"ReturnStatement",
        ??????argument:?{
        ????????type:?"BinaryExpression",
        ????????operator:?"*",
        ????????left:?{
        ??????????type:?"Identifier",
        ??????????name:?"n"
        ????????},
        ????????right:?{
        ??????????type:?"Identifier",
        ??????????name:?"n"
        ????????}
        ??????}
        ????}]
        ??}
        }

        可以看到 AST 的每一層都擁有相似的結(jié)構(gòu),這樣的每一層結(jié)構(gòu)也被叫做 ? 節(jié)點(diǎn)(Node), 一個(gè) AST 可以由單一的節(jié)點(diǎn)或是成百上千個(gè)節(jié)點(diǎn)構(gòu)成,它們組合在一起可以描述用于靜態(tài)分析的程序語法。每個(gè)節(jié)點(diǎn)都有個(gè)字符串的 type 類型,用來表示節(jié)點(diǎn)的類型,babel 中定義了包含所有 JavaScript 語法的類型,如:

        • 聲明語句:如 FunctionDeclaration、VaraibaleDeclaration、ClassDeclaration 等聲明;
        • 標(biāo)識(shí)符:Identifier,變量或函數(shù)參數(shù)。
        • 字面量:StringLiteral、NumbericLiteral、BooleanLiteral 等字面量類型;
        • 語句:WhileStatementReturnStatement 等語句;
        • 表達(dá)式:FunctionExpressionBinaryExpression、AssignmentExpression 等。

        所有的這些節(jié)點(diǎn)通過嵌套形成了 AST 樹,例如一個(gè)變量賦值語句形成的樹形結(jié)構(gòu)如下所示:

        transform(轉(zhuǎn)化)

        transform 階段主要是對(duì)上一步 parse 生成的 AST 進(jìn)行深度優(yōu)先遍歷,從而對(duì)于匹配節(jié)點(diǎn)進(jìn)行增刪改查來修改樹形結(jié)構(gòu)。在 babel 中會(huì)用所配置的 plugin 或 presets 對(duì) AST 進(jìn)行修改后,得到新的 AST,我們的 babel 插件大部分用于這個(gè)階段。

        transform(AST,?BabelPlugins)?=>?newAST

        babel 中通過 @babel/traverse 這個(gè)包來對(duì) AST 進(jìn)行遍歷,找出需要修改的節(jié)點(diǎn)再進(jìn)行轉(zhuǎn)換,這個(gè)過程有點(diǎn)類似我們操作 DOM 樹。當(dāng)我們談及“進(jìn)入”一個(gè)節(jié)點(diǎn),實(shí)際上是說我們?cè)谠L問它們, 之所以使用這樣的術(shù)語是因?yàn)橛幸粋€(gè)訪問者模式(visitor)[32]的概念。訪問者 visitor 是一個(gè)用于 AST 遍歷的跨語言的模式。簡(jiǎn)單的說它就是一個(gè)對(duì)象,定義了用于在一個(gè)樹狀結(jié)構(gòu)中獲取具體節(jié)點(diǎn)的方法。

        visitor 是一個(gè)由各種 type 或者是 enterexit 組成的對(duì)象,完成某種類型節(jié)點(diǎn)的"進(jìn)入"或"退出"兩個(gè)步驟則為一次訪問,在其中可以定義在遍歷 AST 的過程中匹配到某種類型的節(jié)點(diǎn)后該如何操作,目前支持的寫法如下:

        traverse(ast,?{
        ??/*?VisitNodeObject?*/
        ??enter(path,?state)?{},
        ??exit(path,?state)?{},
        ??/*?[Type?in?t.Node["type"]]?*/
        ??Identifier(path,?state)?{},?//?進(jìn)入?Identifier(標(biāo)識(shí)符節(jié)點(diǎn))?節(jié)點(diǎn)時(shí)調(diào)用
        ??StringLiteral:?{?//?進(jìn)入?StringLiteral(字符串節(jié)點(diǎn))?節(jié)點(diǎn)時(shí)調(diào)用
        ????enter(path,?state)?{},?//?進(jìn)入該節(jié)點(diǎn)時(shí)的操作
        ????exit(path,?state)?{},??//?離開該節(jié)點(diǎn)時(shí)的操作
        ??},
        ??'FunctionDeclaration|VariableDeclaration'(path,?state)?{},?//?//?進(jìn)入?FunctionDeclaration?和?VariableDeclaration?節(jié)點(diǎn)時(shí)調(diào)用
        })

        訪問一個(gè)節(jié)點(diǎn)的過程如下:

        下面我們看一個(gè)簡(jiǎn)單的例子:

        const?parser?=?require('@babel/parser')
        const?traverse?=?require('@babel/traverse').default
        const?fs?=?require('fs');

        const?code?=?`function?square(n)?{
        ??return?n?*?n;
        }`;
        const?ast?=?parser.parse(code);
        const?newAst?=?traverse(ast,?{
        ??enter(path)?{
        ????if?(path.isIdentifier({
        ????????name:?"n"
        ??????}))?{
        ??????path.node.name?=?"x";
        ????}
        ??},
        ??FunctionDeclaration:?{
        ????enter()?{
        ??????console.log('enter?function?declaration')
        ????},
        ????exit()?{
        ??????console.log('exit?function?declaration')
        ????}
        ??}
        });

        上面的例子中,通過識(shí)別標(biāo)識(shí)符把 "n" 換成了 "x",其中的 path 是遍歷過程中的路徑,會(huì)保留上下文信息,有很多屬性和方法,可以在訪問到指定節(jié)點(diǎn)后,根據(jù) path 進(jìn)行自定義操作,比如:

        • path.node 指向當(dāng)前 AST 節(jié)點(diǎn),path.parent 指向父級(jí) AST 節(jié)點(diǎn);
        • path.getSibling、path.getNextSibling、path.getPrevSibling 獲取兄弟節(jié)點(diǎn);
        • path.isxxx 判斷當(dāng)前節(jié)點(diǎn)是不是 xx 類型;
        • path.insertBefore、path.insertAfter 插入節(jié)點(diǎn);
        • path.replaceWith、path.replaceWithMultiple、replaceWithSourceString 替換節(jié)點(diǎn);
        • path.skip 跳過當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)的遍歷,path.stop 結(jié)束后續(xù)遍。

        有了 @babel/traverse 我們可以在 tranform 階段做很多自定義的事情,例如刪除 console.log 語句,在特定的地方插入一些表達(dá)式等等,從而影響輸出結(jié)果。我們?cè)倥e一個(gè)例子,在 console 語句中增加位置信息的輸出,形如:console.log('[18,0]', 111)。

        import?*?as?t?from?"@babel/types";?//?用來創(chuàng)建一些?AST?和判斷?AST?的類型

        traverse(ast,?{
        ??visitor:?{
        ????CallExpression(path,?state)?{
        ??????const?callee?=?path.node.callee;
        ??????if?(
        ????????callee.object.name?===?'console'?&&
        ????????['log',?'info',?'error'].includes(callee.property.name)
        ??????)?{
        ????????const?{?line,?column?}?=?path.node.loc.start;
        ????????const?locationNode?=?types.stringLiteral(?`[?${line}?,?${column}?]`?);
        ????????path?.node.arguments.unshift(locationNode);??????}
        ????},
        ??},
        })

        generate(生成)

        AST 轉(zhuǎn)換完之后就要輸出目標(biāo)代碼字符串,這個(gè)階段是個(gè)逆向操作,用新的 AST 來生成我們所需要的代碼,在生成階段本質(zhì)上也是遍歷抽象語法樹,根據(jù)抽象語法樹上每個(gè)節(jié)點(diǎn)的類型和屬性遞歸調(diào)用從而生成對(duì)應(yīng)的字符串代碼,在 babel 中通過 @babel/generator 包的 api 來實(shí)現(xiàn)。

        generate(newAST)?=>?newSourceCode

        還拿前面的 transform 的代碼舉例,如下所示:

        const?parser?=?require('@babel/parser');
        const?traverse?=?require('@babel/traverse').default;
        const?generate?=?require('@babel/generator').default;

        const?code?=?`function?square(n)?{
        ??return?n?*?n;
        }`;

        const?ast?=?parser.parse(code);

        traverse(ast,?{
        ??enter(path)?{
        ????if?(path.isIdentifier({
        ????????name:?"n"
        ??????}))?{
        ??????path.node.name?=?"x";
        ????}
        ??},
        });

        const?output?=?generate(ast,?{},?code);
        console.log(output)

        //?{
        //???code:?'function?square(x)?{?return?x?*?x;}',
        //???map:?null,
        //???rawMappings:?undefined
        //?}

        由上面代碼可以看到,函數(shù)里的變量 "n" 被替換成了 "x"。

        總結(jié)

        編譯原理涉及大量的概念及知識(shí),以實(shí)現(xiàn)完整編譯鏈路的 GCC 編譯器來看,單源代碼就 600w 行,足以說明其水深且寬,但編譯原理帶來的價(jià)值是巨大的,可以說是編譯原理的進(jìn)步才有各種高級(jí)語言百花齊放,進(jìn)而提高軟件行業(yè)生產(chǎn)力。Babel 作為前端工程化領(lǐng)域一個(gè)很重要的工具,明白了其編譯流程,對(duì)諸如其他工具 v8 引擎、 tsc、jsx 模板等前端方面的編譯原理便可以融匯貫通,觸類旁通。

        ???H5-Dooring,讓H5制作更簡(jiǎn)單

        ???

        便內(nèi),對(duì)^_^

        ?點(diǎn)、?~。

        關(guān)號(hào)?趣談前端?獲前端~


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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            好逼天天有| 日剧电影大尺度免费完整版 | 91天天| 91AV三级影院 | 午夜成年视频 | 四虎无码| 色婷婷国产精品 | 麻豆视频免费在线 | 娜露丰满大乳人体欣赏 | 国产人妻绿帽3p国语对白 |