【js】JavaScript parser实现浅析
最近筆者的團(tuán)隊(duì)遷移了webpack2,在遷移過程中,筆者發(fā)現(xiàn)webpack2中有相當(dāng)多的兼容代碼,雖然外界有很多聲音一直在質(zhì)疑作者為什么要破壞性更新,其實(shí)大家也都知道webpack1那種過于“靈活”的配置方式是有待商榷的,所以作者才會(huì)在webpack2上進(jìn)行了很多規(guī)范,但是,筆者卻隱隱的覺得,等到webpack3的時(shí)候,估計(jì)會(huì)有更多的破壞性更新,不然也不會(huì)有這個(gè)webpack2了。于是心中有關(guān)webpack的話題便也擱置了,且等它更穩(wěn)定一些,再談不遲,今天先來講講在劇烈的版本變化中,不變的部分。
大家都知道,webpack是做模塊綁定用的,那么就不得不牽涉到語法解析的內(nèi)容,而且其極高的擴(kuò)展性,也往往需要依賴于語法解析,而在webpack內(nèi)部使用acorn做語法解析,類似的還有babel使用的babylon,今天就帶來兩者的簡(jiǎn)要分析。
官方給兩者的定義都叫做JavaScript parser,內(nèi)部也一致的使用了AST(Abstract syntax tree,即抽象語法樹)的概念。如果對(duì)這個(gè)概念不明白的同學(xué)可以參考WIKIAST的解釋
因?yàn)閎abylon引用了flow,eslint等一些checker,所以整個(gè)項(xiàng)目結(jié)構(gòu)相當(dāng)?shù)囊?guī)范,筆者僅已7.0.0為例:
文件夾目錄如下:
index.js //程序入口,會(huì)調(diào)用parser進(jìn)行初始化 types.js //定義了基本類型和接口 options.js //定義獲取配置的方法以及配置的缺省值 parser //所有的parser都在此
index.js //parser入口類,繼承自 StatementParser 即 ./statement.js
statement.js //聲明StatementParser 繼承自 ExpressionParser 即 ./expression.js
expression.js //聲明ExpressionParser 繼承自 LValParser 即 ./lval.js
lval.js //聲明 LValParser 繼承自 NodeUtils 即 ./node.js
node.js //聲明 NodeUtils 繼承自 UtilParser 即 ./util.js, 同時(shí)還實(shí)現(xiàn)了上一級(jí)目錄中types.js 的nodebase接口為Node類
util.js //聲明 UtilParser 繼承自 Tokenizer 即 ../tokenizer/index.js
location.js //聲明 LocationParser 主要用于拋出異常 繼承自 CommentsParser 即./comments.js
comments.js //聲明 CommentsParser 繼承自 BaseParser 即./base.js
base.js //所有parser的基類
plugins
tokenizer
index.js //定義了 Token類 繼承自上級(jí)目錄parser的LocationParser 即 ../parser/location.js
util
大概流程是這樣的:
1、首先調(diào)用index.js的parse;
2、實(shí)例化一個(gè)parser對(duì)象,調(diào)用parser對(duì)象的parse方法,開始轉(zhuǎn)換;
3、初始化node開始構(gòu)造ast;
1) node.js 初始化node
2) tokenizer.js 初始化token
3) statement.js 調(diào)用 parseBlockBody,開始解析。這個(gè)階段會(huì)構(gòu)造File根節(jié)點(diǎn)和program節(jié)點(diǎn),并在parse完成之后閉合
4) 執(zhí)行parseStatement, 將已經(jīng)合法的節(jié)點(diǎn)插入到body中。這個(gè)階段會(huì)產(chǎn)生各種*Statement type的節(jié)點(diǎn)
5)分解statement, parseExpression。這個(gè)階段除了產(chǎn)生各種expression的節(jié)點(diǎn)以外,還將將產(chǎn)生type為Identifier的節(jié)點(diǎn)
6) 將上步驟中生成的原子表達(dá)式,調(diào)用toAssignable ,將其參數(shù)歸類
4、迭代過程完成后,封閉節(jié)點(diǎn),完成body閉合
不過在筆者看來,babylon的parser實(shí)現(xiàn)似乎并不能稱得上是一個(gè)很好的實(shí)現(xiàn),而實(shí)現(xiàn)中往往還會(huì)使用的forwarddeclaration(類似虛函數(shù)的概念),如下圖
一個(gè)“+”在方法前面的感覺就像是要以前的IIFE一樣。。
有點(diǎn)扯遠(yuǎn)了,總的來說依然是傳統(tǒng)語法分析的幾個(gè)步驟,不過筆者在讀源碼的時(shí)候一直覺得蠻奇怪的為何他們內(nèi)部要使用繼承來實(shí)現(xiàn)parser,parser的場(chǎng)景更像是mixin或者高階函數(shù)的場(chǎng)景,不過后者在具體處理中確實(shí)沒有繼承那樣清晰的結(jié)構(gòu)。
說了這么多,babylon最后會(huì)生成什么呢?以es2016的冪運(yùn)算“3 ** 2”為例:
{
"type": "File",
"start": 0,
"end": 7,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 7
}
},
"program": {
"type": "Program",
"start": 0,
"end": 7,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 7
}
},
"sourceType": "script",
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 7,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 7
}
},
"expression": {
"type": "BinaryExpression",
"start": 0,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 6
}
},
"left": {
"type": "NumericLiteral",
"start": 0,
"end": 1,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 1
}
},
"extra": {
"rawValue": 3,
"raw": "3"
},
"value": 3
},
"operator": "**",
"right": {
"type": "NumericLiteral",
"start": 5,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 6
}
},
"extra": {
"rawValue": 2,
"raw": "2"
},
"value": 2
}
}
}
],
"directives": []
}
}
完整的列表看著未免有些可怕,筆者將有關(guān)location信息的去除之后,構(gòu)造了以下這個(gè)對(duì)象:
{
"type": "File",
"program": {
"type": "Program",
"sourceType": "script",
"body": [{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"left": {
"type": "NumericLiteral",
"value": 3
},
"operator": "**",
"right": {
"type": "NumericLiteral",
"value": 2
}
}
}]
}
}
可以看出,這個(gè)類AST的的對(duì)象是內(nèi)部,大部分內(nèi)容是其實(shí)是有關(guān)位置的信息,因?yàn)楹艽蟪潭壬希枰赃@些信息去描述這個(gè)node的具體作用。
然后讓我們?cè)賮砜纯磜ebpack使用的acorn:
也許是acorn的作者和筆者有類似閱讀babylon的經(jīng)歷,覺得這種實(shí)現(xiàn)不太友好。。于是,acorn的作者用了更為簡(jiǎn)單直接的實(shí)現(xiàn):
index.js //程序入口 引用了 ./state.js 的Parser類 state.js //構(gòu)造Parser類 parseutil.js //向Parser類 添加有關(guān) UtilParser 的方法 statement.js //向Parser類 添加有關(guān) StatementParser 的方法 lval.js //向Parser類 添加有關(guān) LValParser 的方法 expression.js //向Parser類 添加有關(guān) ExpressionParser 的方法 location.js //向Parser類 添加有關(guān) LocationParser 的方法 scope.js //向Parser類 添加處理scope的方法 identifier.js locutil.js node.js options.js tokencontext.js tokenize.js tokentype.js util.js whitespace.js
雖然內(nèi)部實(shí)現(xiàn)基本是類似的,有很多連方法名都是一致的(注釋中使用的類名在acorn中并沒有實(shí)現(xiàn),只是表示具有某種功能的方法的集合),但是在具體實(shí)現(xiàn)上,acorn不可謂不暴力,連多余的目錄都沒有,所有文件全在src目錄下,其中值得一提的是它并沒有使用繼承的方式,而是使用了對(duì)象擴(kuò)展的方式來實(shí)現(xiàn)的Parser類,如下圖:
在具體的文件中,直接擴(kuò)展Paser的prototype
沒想到筆者之前戲談的mixin的方式真的就這樣被使用了,然而mixin的可讀性一定程度上還要差,經(jīng)歷過類似ReactComponentWithPureRenderMixin的同學(xué)想必印象尤深。
不過話說回來,acorn內(nèi)部實(shí)現(xiàn)與babylon并無二致,連調(diào)用的方法名都是類似的,不過acorn多實(shí)現(xiàn)了一個(gè)scope的概念,用于限制作用域。
緊接著我們來看一下acorn生成的結(jié)果,以“x ** y”為例:
{
type: "Program",
body: [{
type: "ExpressionStatement",
expression: {
type: "BinaryExpression",
left: {
type: "Identifier",
name: "x",
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 1
}
}
},
operator: "**",
right: {
type: "Identifier",
name: "y",
loc: {
start: {
line: 1,
column: 5
},
end: {
line: 1,
column: 6
}
}
},
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 6
}
}
},
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 6
}
}
}],
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 6
}
}
}, {
ecmaVersion: 7,
locations: true
}
可以看出,大部分內(nèi)容依然是位置信息,我們照例去掉它們:
{
type: "Program",
body: [{
type: "ExpressionStatement",
expression: {
type: "BinaryExpression",
left: {
type: "Identifier",
name: "x",
},
operator: "**",
right: {
type: "Identifier",
name: "y",
}
}
}]
}
除去一些參數(shù)上的不同,最大的區(qū)別可能就是最外層babylon還有一個(gè)File節(jié)點(diǎn),而acorn的根節(jié)點(diǎn)就是program了,畢竟babel和webpack的工作場(chǎng)景還是略有區(qū)別的。
也許,僅聽筆者講述一切都那么簡(jiǎn)單,然而這只是理想情況,現(xiàn)實(shí)的復(fù)雜遠(yuǎn)超我們的想象,簡(jiǎn)單的舉個(gè)印象比較深的例子,在兩個(gè)parser都有有關(guān)whitespace的抽象,主要是用于提供一些匹配換行符的正則,通常都想到的是:
/ ?| /
但實(shí)際上完整的卻是
/ ?| |u2028|u2029/
而且考慮到ASCII碼的情況,還需要很糾結(jié)的枚舉出非空格的情況
/[u1680u180eu2000-u200au202fu205fu3000ufeff]/
因?yàn)閜arse處理的是我們實(shí)際開發(fā)中自己coding的代碼,不同的人不同的風(fēng)格,會(huì)有怎么樣的奇怪的方式其實(shí)是非常考驗(yàn)完備性思維的一項(xiàng)工作,而且這往往比我們?nèi)粘5臉I(yè)務(wù)工作的場(chǎng)景更為復(fù)雜,它很多時(shí)候甚至是接近一個(gè)可能性的全集,而并非“大概率可能”的一個(gè)集合。雖然我們?nèi)粘9ぷ鬟@種parser幾乎是透明的,我們?cè)趇nit的前端項(xiàng)目時(shí)基本已經(jīng)部署好了開發(fā)環(huán)境,但是對(duì)于某些情況下的實(shí)際問題定位,卻又有非凡的意義,而且,這還在一定時(shí)間內(nèi)是一個(gè)常態(tài),雖然可能在不久的未來,就會(huì)有更加智能更加強(qiáng)大的前端IDE。
有關(guān)ast的實(shí)驗(yàn),可以試一下這個(gè)網(wǎng)站:https://astexplorer.net/
總結(jié)
以上是生活随笔為你收集整理的【js】JavaScript parser实现浅析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UIview需要知道的一些事情:setN
- 下一篇: POI 实现 word转成pdf