成都創(chuàng)新互聯(lián)一直在為企業(yè)提供服務(wù),多年的磨煉,使我們?cè)趧?chuàng)意設(shè)計(jì),成都全網(wǎng)營(yíng)銷到技術(shù)研發(fā)擁有了開發(fā)經(jīng)驗(yàn)。我們擅長(zhǎng)傾聽企業(yè)需求,挖掘用戶對(duì)產(chǎn)品需求服務(wù)價(jià)值,為企業(yè)制作有用的創(chuàng)意設(shè)計(jì)體驗(yàn)。核心團(tuán)隊(duì)擁有超過十年以上行業(yè)經(jīng)驗(yàn),涵蓋創(chuàng)意,策化,開發(fā)等專業(yè)領(lǐng)域,公司涉及領(lǐng)域有基礎(chǔ)互聯(lián)網(wǎng)服務(wù)成都多線機(jī)房、手機(jī)APP定制開發(fā)、手機(jī)移動(dòng)建站、網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)絡(luò)整合營(yíng)銷。
普遍的觀點(diǎn)認(rèn)為,前端就是打好 HTML、CSS、JS 三大基礎(chǔ),深刻理解語(yǔ)義化標(biāo)簽,了解 N 種不同的布局方式,掌握語(yǔ)言的語(yǔ)法、特性、內(nèi)置 API。再學(xué)習(xí)一些主流的前端框架,使用社區(qū)成熟的腳手架,即可快速搭建一個(gè)前端項(xiàng)目。勝任前端工作非常容易。再往深處學(xué)習(xí),你會(huì)發(fā)現(xiàn)前端這個(gè)領(lǐng)域,總是有學(xué)不完的框架、工具、庫(kù),不斷有新的輪子出現(xiàn)。技術(shù)推陳出新,版本快速迭代,但萬(wàn)變不離其宗。工具致力于流程自動(dòng)化、規(guī)范化,服務(wù)于簡(jiǎn)潔、優(yōu)雅、高效的編碼,將問題高度抽象化、層次化。在如今前端開源界如此火熱的現(xiàn)狀下,框架的使用者與框架的維護(hù)者聯(lián)系更加緊密,不僅能深入源碼來(lái)更徹底地認(rèn)識(shí)框架,還能夠提出問題,參與討論,貢獻(xiàn)代碼,共同解決技術(shù)問題,推進(jìn)前端生態(tài)的發(fā)展和壯大。而編譯原理,作為一門基礎(chǔ)理論學(xué)科,除了 JS 語(yǔ)言本身的編譯器之外,更成為 Babel、ESLint、Stylus、Flow、Pug、YAML、Vue、React、Marked 等開源前端框架的理論基石之一。了解編譯原理能夠?qū)λ佑|的框架有更充分的認(rèn)識(shí)。
對(duì)外部來(lái)說,編譯器是一個(gè)黑盒子,能夠把一種源語(yǔ)言翻譯為語(yǔ)義上等價(jià)的另一種目標(biāo)語(yǔ)言。從現(xiàn)代高級(jí)編譯器的角度講,源語(yǔ)言是高級(jí)程序設(shè)計(jì)語(yǔ)言,容易閱讀與編寫,而目標(biāo)語(yǔ)言是機(jī)器語(yǔ)言,即二進(jìn)制代碼,能夠被計(jì)算機(jī)直接識(shí)別。從語(yǔ)言系統(tǒng)的處理角度來(lái)看,由源程序生成可執(zhí)行程序的整體工作流程如圖 1 所示:
圖1 源程序生成可執(zhí)行程序整體工作流程圖
其中,編譯器又分為前端和后端兩個(gè)部分。前端包括詞法分析、語(yǔ)法分析、語(yǔ)義分析、中間代碼生成,具有機(jī)器無(wú)關(guān)性,比較有代表性的工具是 Flex、Bison。后端包括中間代碼優(yōu)化、目標(biāo)代碼生成,具有機(jī)器相關(guān)性,比較有代表性的工具是 LLVM。在 Web 前端工程領(lǐng)域,由于宿主環(huán)境瀏覽器與 Node.js 的跨平臺(tái)特性,我們只需關(guān)注編譯器前端部分,就可以充分發(fā)揮它的應(yīng)用價(jià)值。為了更好地理解編譯器前端的工作原理,本文將主要以目前被廣泛使用的 Babel 為例,闡述它是如何將源代碼編譯為目標(biāo)代碼。
作為新生代 ES 語(yǔ)法編譯器,Babel 在前端工具鏈中占據(jù)了非常重要的地位,它嚴(yán)格按照 ECMA-262 語(yǔ)言規(guī)范,實(shí)現(xiàn)對(duì)最新語(yǔ)法的解析,而無(wú)需等待瀏覽器升級(jí)來(lái)提供對(duì)新特性的支持。Babel 內(nèi)部所使用的語(yǔ)法解析器是 Babylon,抽象語(yǔ)法樹(簡(jiǎn)寫為 AST)的結(jié)點(diǎn)類型定義則參考了 Mozilla JS 引擎 SpiderMonkey,并對(duì)其進(jìn)行擴(kuò)展增強(qiáng),且支持對(duì) Flow、JSX、TypeScript 語(yǔ)法的解析。它所使用的 Babylon 實(shí)現(xiàn)了編譯器中兩個(gè)部分,詞法分析和語(yǔ)法分析。
詞法分析
詞法分析是處理源程序的第一部分,主要任務(wù)是逐個(gè)掃描輸入字符,轉(zhuǎn)換為詞法單元(Token)序列,傳遞給語(yǔ)法分析器進(jìn)行語(yǔ)法分析。Token 是一個(gè)不可分割的最小單元。例如 var 這三個(gè)字符,它只能作為一個(gè)整體,語(yǔ)義上不能再被分解,因此它是一個(gè) Token。每個(gè) Token 對(duì)象都有能夠被單獨(dú)識(shí)別的類型屬性和其它附加屬性(操作符優(yōu)先級(jí)、行列號(hào)等)。在 Babylon 詞法分析器里,每個(gè)關(guān)鍵字是一個(gè) Token ,每個(gè)標(biāo)識(shí)符是一個(gè) Token,每個(gè)操作符是一個(gè) Token,每個(gè)標(biāo)點(diǎn)符號(hào)也都是一個(gè) Token。除此之外,還會(huì)過濾掉源程序中的注釋和空白字符(換行符、空格、制表符等)。
對(duì)于 Token 的匹配規(guī)則,可以根據(jù)正則表達(dá)式來(lái)描述。舉個(gè)例子,要匹配一個(gè) Number 類型的 Token,可以檢測(cè)是否以 [0-9] 開頭,接著循環(huán)或遞歸掃描緊連的后續(xù)字符,且需要特別留意 0b、0o、0x 開頭的非十進(jìn)制數(shù)值、科學(xué)計(jì)數(shù)法 e 或 E、小數(shù)點(diǎn)等特殊字符,指針不斷后移直至不滿足匹配規(guī)則或者到達(dá)行末尾。最后生成一個(gè) Number 類型的 Token,附帶值、文件位置等屬性,并加入到 Token 序列中,繼續(xù)下一輪掃描。
一個(gè)簡(jiǎn)單的 Number 類型狀態(tài)轉(zhuǎn)換如圖 2 所示:
圖2 Number 類型狀態(tài)轉(zhuǎn)換示意圖
當(dāng)然除了 Babylon 手寫詞法分析器之外,這個(gè)過程還可以采用有窮自動(dòng)機(jī)(DFA/NFA)的方式實(shí)現(xiàn),通過詞法分析器生成器,把輸入程序(模式匹配規(guī)則)自動(dòng)轉(zhuǎn)換成一個(gè)詞法分析器,這里不展開闡述。
語(yǔ)法分析
語(yǔ)法分析是詞法分析的下一步,主要任務(wù)是掃描來(lái)自詞法分析器產(chǎn)生的 Token 序列,根據(jù)文法和結(jié)點(diǎn)類型定義構(gòu)造出一棵 AST,傳遞給編譯器前端余下部分。文法描述了程序設(shè)計(jì)語(yǔ)言的構(gòu)造規(guī)則,用于指導(dǎo)整個(gè)語(yǔ)法分析的過程。它由四個(gè)部分組成,一組終結(jié)符號(hào)(也稱 Token)、一組非終結(jié)符號(hào)、一組產(chǎn)生式和一個(gè)開始符號(hào)。例如,函數(shù)聲明語(yǔ)句的產(chǎn)生式表示形式如圖 3 所示:
圖3 函數(shù)聲明語(yǔ)句的產(chǎn)生式
根據(jù)文法,語(yǔ)法分析器將 Token 逐個(gè)讀入,不斷替換文法產(chǎn)生式體的非終結(jié)符號(hào),直至全部將非終結(jié)符號(hào)替換為終結(jié)符號(hào),這個(gè)過程被稱為推導(dǎo)。推導(dǎo)又分為兩種方式,最左推導(dǎo)和最右推導(dǎo)。如果總是優(yōu)先替換產(chǎn)生式體最左側(cè)的非終結(jié)符號(hào),被稱為最左推導(dǎo),如果總是優(yōu)先替換產(chǎn)生式體最右側(cè)的非終結(jié)符號(hào),被稱為最右推導(dǎo)。
語(yǔ)法分析器按照工作方式來(lái)劃分,分為自頂向下分析法和自底向上分析法。自頂向下分析法要求通過最左推導(dǎo)從頂部 ( 根結(jié)點(diǎn) ) 開始構(gòu)造 AST,常用的分析器有遞歸下降語(yǔ)法分析器、 LL 語(yǔ)法分析器。而自底向上分析法要求通過最右推導(dǎo)從底部 ( 葉子結(jié)點(diǎn) ) 開始構(gòu)造 AST,常用的分析器有 LR 語(yǔ)法分析器、SLR 語(yǔ)法分析器、LALR 語(yǔ)法分析器。這兩種分析方式在 Babylon 中都有所實(shí)踐。
首先是自頂向下分析法,例如變量聲明語(yǔ)句:
var?foo?=?"bar";
經(jīng)由詞法分析器處理后,會(huì)生成 Token 序列:
Token('var')Token('foo')Token('=')Token('"bar"')Token(';')
由 LL(1) 語(yǔ)法分析器進(jìn)行遞歸下降分析,每次向前查看一個(gè)輸入 Token,來(lái)決定該用哪種產(chǎn)生式展開。對(duì)于變量聲明語(yǔ)句的 FIRST 集合(推導(dǎo)結(jié)果的首個(gè) Token 集合),只需檢查輸入 Token 為 Token('var')、Token('let')、Token('const') 三者其中之一,那么就使用該產(chǎn)生式展開。首先構(gòu)造 AST 最頂層結(jié)點(diǎn) VariableDeclaration,把 Token('var') 的值加入到該結(jié)點(diǎn)屬性中, 接著逐個(gè)讀入其余 Token,根據(jù)產(chǎn)生式的非終結(jié)符號(hào)從左到右的順序,依次構(gòu)造它的子結(jié)點(diǎn),不斷遞歸下降分析,直至所有 Token 讀入完畢。最后生成的一棵 AST 如圖 4 所示:
圖4 自頂向下分析法產(chǎn)生的 AST 樹
另一種是自底向上分析法,例如成員表達(dá)式語(yǔ)句:
foo.bar.baz.qux
我們都知道這條語(yǔ)句等價(jià)于:
((foo.bar).baz).qux
而不是:
foo.(bar.(baz.qux))
原因就在于它所設(shè)計(jì)的文法是左遞歸的,而 LL 語(yǔ)法分析器是無(wú)法做到解析左遞歸的文法,這時(shí)候只能使用 LR 語(yǔ)法分析器的方式,自底向上地構(gòu)造 AST。LR 語(yǔ)法分析器的核心是移入 - 歸約分析技術(shù),通過維護(hù)一個(gè)棧,由下一個(gè)輸入 Token 來(lái)決定是把它移入棧中還是將棧頂?shù)牟糠址?hào)進(jìn)行歸約(把產(chǎn)生式體替換為產(chǎn)生式頭),先構(gòu)造子結(jié)點(diǎn),再構(gòu)造父結(jié)點(diǎn),直至棧中所有符號(hào)全部歸約。最后生成的一棵 AST 如圖 5 所示:
圖5 自底向上分析法產(chǎn)生的 AST 樹
此外,由 Babylon 構(gòu)建的完整的 AST 還擁有特殊頂層結(jié)點(diǎn) File 和 Program,它們描述了文件的基本信息、模塊類型等等。
生成代碼
工業(yè)級(jí)別的語(yǔ)言編譯器,通常還會(huì)有語(yǔ)義分析階段,檢查程序上下文是否和語(yǔ)言所定義的語(yǔ)義一致,比如類型檢查,作用域檢查,另一個(gè)則是生成中間代碼,比如三地址代碼,用地址和指令來(lái)線性描述程序。但由于 Babel 的定位僅僅是對(duì) ES 語(yǔ)法的轉(zhuǎn)換,這一部分工作可以交給 JS 解釋器引擎來(lái)處理。而 Babel 最為特色的部分是它的插件機(jī)制,針對(duì)不同的瀏覽器版本環(huán)境,調(diào)用不同的 Babel 插件。通過訪問者模式(一種設(shè)計(jì)模式)的接口定義,對(duì) AST 進(jìn)行一遍深度優(yōu)先遍歷,對(duì)指定的匹配到的結(jié)點(diǎn)進(jìn)行修改、刪除、新增、移位,使原先的 AST 轉(zhuǎn)換為另一棵經(jīng)過修改的 AST。
一個(gè)訪問者模式的接口定義如下:
visitor:?{ ??Identifier(path)?{ ????enter()?{ ??????//遍歷AST進(jìn)入Identifier結(jié)點(diǎn)時(shí)執(zhí)行??????...? ????}, ????exit()?{ ??????//遍歷AST離開Identifier結(jié)點(diǎn)時(shí)執(zhí)行??????... ????} ??}, ??...}
最后一個(gè)階段則是生成目標(biāo)代碼,從 AST 的根結(jié)點(diǎn)出發(fā),遞歸下降遍歷,對(duì)每個(gè)結(jié)點(diǎn)都調(diào)用一個(gè)相關(guān)函數(shù),執(zhí)行語(yǔ)義動(dòng)作,不斷打印代碼片段,最終生成目標(biāo)代碼,即經(jīng)過 babel 編譯后的代碼。
再講到模板引擎,最早誕生于服務(wù)端動(dòng)態(tài)頁(yè)面的開發(fā),如 JSP、PHP、ASP 等模板引擎,自 Node.js 快速發(fā)展以后,前端界又產(chǎn)出了非常多的輪子,包括 EJS、Handlebars、Pug (前身為 Jade)、Mustache 等等,數(shù)不勝數(shù)。模板引擎技術(shù)使得結(jié)合數(shù)據(jù)渲染視圖變得更加靈活,給邏輯的抽象帶來(lái)了更多的可能性,數(shù)據(jù)與內(nèi)容互不依賴。模板引擎的實(shí)現(xiàn)方式有很多種,比較簡(jiǎn)單的模板引擎,直接利用字符串替換、拼接的方式實(shí)現(xiàn),比較復(fù)雜的模板引擎,例如 Pug,則會(huì)有比較完整的詞法分析和語(yǔ)法分析過程,將模板預(yù)編譯成 JS 代碼再去動(dòng)態(tài)執(zhí)行。
例如模板語(yǔ)句:
h2?hello?#{name}
經(jīng)由 Pug 解析器生成的 AST 如圖 6 所示:
圖6 由 Pug 解析器生成的 AST
生成器生成的目標(biāo)代碼為(偽代碼):
''?+?'hello'?+?name?+?'
'
運(yùn)行時(shí)再調(diào)用 new Function 來(lái)動(dòng)態(tài)執(zhí)行代碼:
var?compiledFn?=?new?Function('local',?`??with?(local)?{????return?''?+?'hello'?+?name?+?'
';? ??}`)compiledFn({ ??name:?'world'})
最后輸出 HTML 語(yǔ)句:
hello?world
整個(gè)過程由兩部分組成,預(yù)編譯階段和運(yùn)行時(shí)階段。當(dāng)然一個(gè)好的模板引擎還會(huì)考慮功能、性能與安全兼?zhèn)?,上面的`with`語(yǔ)句是要避免的,還要引入緩存機(jī)制,XSS 防范機(jī)制,以及更加強(qiáng)大、友好、易于使用的語(yǔ)法糖。
另外值得一提的是以 Angular、React、Vue 為代表的前端 MVVM 框架,無(wú)一不引入了模板編譯技術(shù)。Vue 作為漸進(jìn)式的前端解決方案,受到眾多開發(fā)者們的青睞,它對(duì)視圖的渲染提供了渲染函數(shù)和模板兩種方式。使用渲染函數(shù)需要調(diào)用核心 API 來(lái)構(gòu)建 Virtual DOM 類型,過程相對(duì)復(fù)雜,編碼量非常大,一旦 DOM 層次嵌套過深,就會(huì)造成代碼難以掌控和維護(hù)的局面。為了應(yīng)對(duì)這種復(fù)雜性,另一種方式則是編寫基于 HTML 的模板,并加入 Vue 特有的標(biāo)簽、指令、插值等語(yǔ)法,由編譯器來(lái)進(jìn)行從模板到渲染函數(shù)的編譯和優(yōu)化,相對(duì)前者更優(yōu)雅、便捷、易于編碼。
前端布局方式從刀耕火種的純 CSS 年代演進(jìn)到以 Sass、Less、Stylus 為代表的預(yù)處理語(yǔ)言,賦予了 CSS 可編程的能力,定義變量,函數(shù),表達(dá)式計(jì)算、模塊化等特性,極大地提升了開發(fā)人員的生產(chǎn)效率。這些都是編譯技術(shù)所帶來(lái)的變化。同樣,編譯器對(duì)原樣式代碼進(jìn)行詞法分析,產(chǎn)生 Token 序列。接著,語(yǔ)法分析,生成中間表示,一棵符合定義的 AST。同時(shí),還會(huì)為每個(gè)程序塊建立一個(gè)符號(hào)表來(lái)記錄變量的名字,屬性,為代碼生成階段的變量作用域分析提供幫助。最后,遞歸下降訪問 AST,生成能夠在瀏覽器環(huán)境中直接執(zhí)行的 CSS 代碼。
以預(yù)處理器 Stylus 語(yǔ)法為例:
foo?=?14pxbody ??font-size?foo
編譯生成的 AST 為圖 7 所示:
圖7 由 Stylus 解析器生成的 AST
最后生成的目標(biāo)代碼為:
body?{ ??font-size:?14px;}
看似簡(jiǎn)單容易的代碼轉(zhuǎn)換背后,編譯器為我們做了許多語(yǔ)法層面的處理,給 CSS 帶來(lái)了從未有過的強(qiáng)大的擴(kuò)展能力,以及底層對(duì)編譯速度的持續(xù)優(yōu)化,讓 CSS 的編寫方式更加簡(jiǎn)潔高效,易于維護(hù)和管理。
寫這篇文章的目的是希望告訴讀者,編譯原理在前端工程領(lǐng)域的應(yīng)用非常廣泛,可以用來(lái)幫助我們解決工程技術(shù)上的難點(diǎn)。當(dāng)然在實(shí)際編碼過程中,需要非常得有耐心,細(xì)心,考慮各種文法,分析方式,優(yōu)化手段,寫好測(cè)試用例等等。一個(gè)良好的編譯器需要精心打磨,不斷優(yōu)化升級(jí),全方位為開發(fā)者服務(wù)。如果你沒有學(xué)習(xí)過編譯原理相關(guān)知識(shí),建議尋找相關(guān)書籍,系統(tǒng)地學(xué)習(xí)一遍知識(shí)體系。即使在實(shí)際日常工作中接觸不到編譯原理,但它對(duì)基礎(chǔ)知識(shí)的積累與掌握,對(duì)編程語(yǔ)言的認(rèn)識(shí)與理解,對(duì)框架的學(xué)習(xí)與運(yùn)用,對(duì)日后職業(yè)生涯的發(fā)展道路,或多或少都有幫助。