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