本文將經過如下幾個方面對AST進行學習javascript
AST(抽象語法樹)在開發過程當中扮演一個很是重要的角色,可是咱們卻不多去直接接觸它。css
不管是代碼編譯(babel),打包(webpack),代碼壓縮,css預處理,代碼校驗(eslint),代碼美化(pretiier),Vue中對template的編譯,這些的實現都離不開AST。前端
瞭解學習AST,可以幫助咱們更好的對上面說的這些工具原理進行理解,同時,咱們能夠利用它去開發一些工具,來優化咱們的開發流程,提升開發效率。vue
AST是對源代碼的抽象語法結構的樹狀表現形式。java
在不一樣的場景下,會有不一樣的解析器將源碼解析成抽象語法樹。webpack
下面直觀的看一下AST是什麼樣的es6
代碼web
let answer = 2 * 3;
複製代碼
對應的AST語法樹正則表達式
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Literal",
"value": 2,
"raw": "2"
},
"right": {
"type": "Literal",
"value": 3,
"raw": "3"
}
}
}
],
"kind": "let"
}
],
"sourceType": "script"
}
複製代碼
那麼AST是如何生成的呢?express
AST是經過JS Parser (解析器),將js源碼轉化爲抽象語法樹,主要分爲兩步
將整個的代碼字符串,分割成語法單元數組(token)。 JS中的語法單元(token)指標識符(function,return),運算符,括號,數字,字符串等能解析的最小單元。主要有如下幾種:
標識符
沒有被引號括起來的連續字符,能夠包含字母、數字、_、$,其中數字不能做爲開頭。
標識符多是var,return,function等關鍵字,也多是true,false這樣的內置常量,或是一個變量。具體是哪一種語義,分詞階段不區分,只要正確拆分便可。
數字 十六進制,十進制,八進制以及科學表達式等都是最小單元
運算符: +、-、 *、/ 等
字符串 對計算機而言,字符串只會參與計算和展現,具體裏面細分不必分析
註釋 不論是行註釋仍是塊註釋,對於計算機來講並不關心其內容,因此能夠做爲不可再拆分的最小單元
空格 連續的空格,換行,縮進等,只要不在字符串中都沒有實際的邏輯意義,因此連續的空格能夠做爲一個語法單元。
其餘,大括號,中括號,小括號,冒號 等等。
依然拿上面的代碼做爲例子,分詞後生成的語法單元數組以下
[
{
"type": "Keyword",
"value": "var",
"range": [
0,
3
]
},
{
"type": "Identifier",
"value": "answer",
"range": [
4,
10
]
},
{
"type": "Punctuator",
"value": "=",
"range": [
11,
12
]
},
{
"type": "Numeric",
"value": "2",
"range": [
13,
14
]
},
{
"type": "Punctuator",
"value": "*",
"range": [
15,
16
]
},
{
"type": "Numeric",
"value": "3",
"range": [
17,
18
]
},
{
"type": "Punctuator",
"value": ";",
"range": [
18,
19
]
}
]
複製代碼
語義分析的目的是將分詞獲得的語法單元進行一個總體的組合,分析肯定語法單元之間的關係。
簡單來講,語義分析能夠理解成對語句(statement)和表達式(expression)的識別。
var a = 1; if(xxx){xxx}
a++
, i > 0 && i< 6
語義分析是一個遞歸的過程,它會將分詞分析出來的數組轉化成樹形的表達形式。同時,會驗證語法,語法若是存在錯誤的話,會拋出語法錯誤。
文章一開始就說到了,babel,webpack,css預處理,eslint等都應用到了AST樹,那麼AST到底作了一個什麼樣的角色呢!? 下面咱們就來看一下。
首先看一下babel工做原理的實現。
babel是一個javascript編譯器,用來將es6語法編譯成es5
babel的工做能夠分爲3個階段:
第1步 解析(Parse)
經過解析器babylon將代碼解析成抽象語法樹
第2步 轉換(TransForm)
經過babel-traverse plugin對抽象語法樹進行深度優先遍歷,遇到須要轉換的,就直接在AST對象上對節點進行添加、更新及移除操做,好比遇到箭頭函數,就轉換成普通函數,最後獲得新的AST樹。
第3步 生成(Generate)
經過babel-generator將AST樹生成es5代碼
Vue 提供了 2 個版本,一個是 Runtime + Compiler ,另外一個是 Runtime only 的,前者是包含編譯代碼的,會把編譯的過程放在運行時作,後者是不包含編譯代碼的,須要藉助 webpack 的vue-loader把模板編譯render函數。無論使用哪一個版本,都有一個環節,就是將模板編譯成render函數。
下面咱們分析下vue模板的編譯過程,這也是vue源碼實現中很是重要的一個模塊。 vue模板的編譯過程分爲3個階段
第1步 解析(Parse)
const ast = parse(template.trim(), options)
複製代碼
將模板字符串解析生成 AST,這裏的解析器是vue本身實現的,解析過程當中會使用正則表達式對模板順序解析,當解析到開始標籤、閉合標籤、文本的時候都會有相對應的回調函數執行,來達到構造 AST 樹的目的。
生成的AST 元素節點總共有 3 種類型,1 爲普通元素, 2 爲表達式,3爲純文本。下面看一個例子
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
複製代碼
上面模板解析生成的AST樹以下:
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
]
]
}]
}
複製代碼
第2步 優化語法樹(Optimize)
optimize(ast, options)
複製代碼
vue模板中並非全部數據都是響應式的,有不少數據是首次渲染後就永遠不會變化的,那麼這部分數據生成的 DOM 也不會變化,咱們能夠在patch的過程跳過對他們的比對。
此階段會深度遍歷生成的 AST樹,檢測它的每一顆子樹是否是靜態節點,若是是靜態節點則它們生成 DOM 永遠不須要改變,這對運行時對模板的更新起到極大的優化做用。
遍歷過程當中,會對整個 AST 樹中的每個 AST 元素節點標記static和staticRoot(遞歸該節點的全部children,一旦子節點有不是static的狀況,則爲false,不然爲true)。
通過該階段,上面例子中的ast會變成
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'static': false,
'staticRoot': false,
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'static': false,
'staticRoot': false,
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
],
'static': false
]
}]
}
複製代碼
第3步 生成代碼
const code = generate(ast, options)
複製代碼
經過generate方法,將ast生成render函數
with(this){
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return _c('li', {
on: {
"click": function($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
}
複製代碼
經過上面對babel實現原理和vue模板的編譯原理能夠看出,他們的實現有不少相同之處,都是先將源碼解析成AST樹,而後對AST樹就行處理,最後生成想要的東西。
Prettier的實現一樣是這樣,首先依然是將代碼解析生成AST樹,而後是對AST遍歷,調整長句,整理空格,括號等,最後輸出代碼,這裏就不贅述了。
咱們分析了Babel原理、vue模板編譯過程、Prettier原理,這裏咱們簡單總結一下。
若是把源碼比做一個機器,那麼分詞過程就是將這臺機器拆分紅一個個零件,語義分析過程就是分析每一個零件的位置以及做用,而後根據須要對零件進行加工處理,最後再組裝成一個新的機器。
那麼工做中咱們能使用AST作些什麼呢?!
這裏就要發揮想象了,看看咱們平常工做中有什麼需求是能夠經過AST開發個工具來解決。
好比,能夠經過AST能夠將代碼自動轉成流程圖;
或者根據自定義的註釋規範,經過工具自動生成文檔;
或是經過工具自動生成骨架屏文件。
你還有什麼好想法呢?
歡迎關注個人公衆號「前端小苑」,我會按期在上面更新原創文章。也能夠加我微信 yu_shihu_
共同交流技術。