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