本文概要javascript
本文將經過如下幾個方面對AST進行學習:css
1. 爲何要了解AST,簡要說明AST在開發中的重要性;html
AST(抽象語法樹)在開發過程當中扮演一個很是重要的角色,可是咱們卻不多去直接接觸它。vue
不管是代碼編譯(babel),打包(webpack),代碼壓縮,css預處理,代碼校驗(eslint),代碼美化(pretiier),Vue中對template的編譯,這些的實現都離不開AST。java
瞭解學習AST,可以幫助咱們更好的對上面說的這些工具原理進行理解,同時,咱們能夠利用它去開發一些工具,來優化咱們的開發流程,提升開發效率。webpack
AST是對源代碼的抽象語法結構的樹狀表現形式。es6
在不一樣的場景下,會有不一樣的解析器將源碼解析成抽象語法樹。web
下面直觀的看一下AST是什麼樣的。正則表達式
代碼:express
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是如何生成的呢?
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個階段:
經過解析器babylon將代碼解析成抽象語法樹。
經過 babel-traverse
plugin 對抽象語法樹進行深度優先遍歷,遇到須要轉換的,就直接在AST對象上對節點進行添加、更新及移除操做,好比遇到箭頭函數,就轉換成普通函數,最後獲得新的AST樹。
經過 babel-generator
將AST樹生成es5代碼。
Vue 提供了 2 個版本,一個是 Runtime + Compiler ,另外一個是 Runtime only 的,前者是包含編譯代碼的,會把編譯的過程放在運行時作,後者是不包含編譯代碼的,須要藉助 webpack 的vue-loader把模板編譯render函數。無論使用哪一個版本,都有一個環節,就是將模板編譯成render函數。
下面咱們分析下vue模板的編譯過程,這也是vue源碼實現中很是重要的一個模塊。
vue模板的編譯過程分爲3個階段:
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'} ] ] }] }
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 ] }] }
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能夠將代碼自動轉成流程圖;
或者根據自定義的註釋規範,經過工具自動生成文檔;
或是經過工具自動生成骨架屏文件。
你還有什麼好想法呢?