走進AST

前言:AST已經深刻的存在咱們項目腳手架中,可是咱們缺不瞭解他,本文帶領你們一塊兒體驗AST,感覺一下解決問題另外一種方法

什麼是AST

在講以前先簡單介紹一下什麼AST,抽象語法樹(Abstract Syntax Tree)簡稱 AST,是源代碼的抽象語法結構的樹狀表現形式。
平時不少庫都有他的影子:
image.png
例如 babel, es-lint, node-sass, webpack 等等。javascript

OK 讓咱們看下代碼轉換成 AST 是什麼樣子。css

const ast = 'tree'

這是一行簡單的聲明代碼,咱們看下他轉換成AST的樣子java

image.png

咱們發現整個樹的根節點是 Program,他有一個子節點 bodybody 是一個數組,數組中還有一個子節點 VariableDeclarationVariableDeclaration中表示const ast = 'tree'這行代碼的聲明,具體的解析以下:node

type: 描述語句的類型,此處是一個變量聲明類型
kind: 描述聲明類型,相似的值有'var' 'let'
declarations: 聲明內容的數組,其中每一項都是一個對象
------------type: 描述語句的類型,此處是一個變量聲明類型
------------id: 被聲明字段的描述
----------------type: 描述語句的類型,這裏是一個標識符
----------------name: 變量的名字
------------init: 變量初始化值的描述
----------------type: 描述語句的類型,這裏是一個標識符
----------------name: 變量的值

大致上的結構是這樣,body下的每一個節點還有一些字段沒有給你們說明,例如:位置信息,以及一些沒有值的key都作了隱藏,推薦你們能夠去 asteplorer這個網站去試試看。webpack

總結一下, AST就是把代碼經過編譯器變成樹形的表達形式。git

如何生成AST

如何生成把純文本的代碼變成AST呢?編輯器生成語法樹通常分爲三個步驟es6

  • 詞法分析
  • 語法分析
  • 生成語法樹
  1. 詞法分析:也叫作掃描。它讀取咱們的代碼,而後把它們按照預約的規則合併成一個個的標識tokens。同時,它會移除空白符,註釋,等。最後,整個代碼將被分割進一個tokens列表(或者說一維數組)。

比方說上面的例子 const ast = 'tree',會被分析爲const、ast、=、'tree'github

const ast = 'tree';
[  
 { type: 'keyword', value: 'const' },  
 { type: 'identifier', value: 'a' },  
 { type: 'punctuator', value: '=' },  
 { type: 'numeric', value: '2' },  
]

當詞法分析源代碼的時候,它會一個一個字母地讀取代碼,因此很形象地稱之爲掃描-scans;當它遇到空格,操做符,或者特殊符號的時候,它會認爲一個話已經完成了。web

2.語法分析:也稱爲解析器。它會將詞法分析出來的數組轉化成樹形的表達形式。同時,驗證語法,語法若是有錯的話,拋出語法錯誤。數組

3.生成樹:當生成樹的時候,解析器會刪除一些不必的標識tokens(好比不完整的括號),所以AST不是100%與源碼匹配的,可是已經能讓咱們知道如何處理了。說個題外話,解析器100%覆蓋全部代碼結構生成樹叫作CST(具體語法樹)

可否經過第三方庫來生成?

有不少的第三方庫能夠用來實戰操做,能夠去asteplorer這個網站去找你喜歡的第三方庫,這裏不限於javascript,其餘的語言也能夠在這個網站上找到。
如圖:
image.png

關於javascript 的第三方庫,這裏給你們推薦 babel 的核心庫babylon

// yarn add babylon
import * as babylon from 'babylon';

const code = `
    const ast = 'tree'
`

const ast = babylon.parse(code); // ast

如何實踐

ok,如今咱們已經知道如何把咱們的代碼變成 AST 了,可是現實中,咱們常常會使用到代碼的轉換,比方說 jsx -> js, es6 -> es5, 是的就是 babel,咱們來看看babel是如何轉換代碼的。

大致上babel轉換代碼分爲三步

1. 經過`babylon`生成`AST`
2. 遍歷`AST`同時經過指定的訪問器訪問須要修改的節點
3. 生成代碼

看一個簡單的例子一塊兒理解一下
生成AST

import * as babylon from 'babylon';
// 這裏也可使用 import parser from '@babel/parser'; 這個來生成語法樹
const code = `
    const ast = 'tree'
    console.log(ast);
`

const ast = babylon.parse(code); // ast

遍歷AST同時經過訪問器CallExpression來訪問console.log(ast)並刪除它

import traverse from '@babel/traverse'
import t from '@babel/types';
// 2 遍歷

const visitor = {

    CallExpression(path) {
        const { callee } = path.node;
        if (
            t.isMemberExpression(callee) &&
            callee.object.name === 'console' &&
            callee.property.name === 'log'
        ) {
            path.remove();
        }
    },
}

traverse.default(ast, visitor);

生成新代碼

import generator from '@babel/generator';
generator.default(ast);
簡單的答疑:CallExpression表示這是一個調用,爲何還要作更深刻的判斷呢,由於直接的函數調用 foo() 這也是一個CallExpression,A.foo()這也是一個CallExpression, 因此要更深刻的判斷

好的,代碼轉換完成!值得慶祝。咱們能夠看到第一步生成AST第三步生成新代碼都由babel替咱們作了,咱們真正操做的地方在於第二步:經過訪問器操做須要操做的節點。

因而可知咱們開發babel-plugin的時候,也只須要關注visitor這部分就好。

上述代碼改成babel-plugin示例:

module.export = function plugin({ types: t}) {
    return {
        visitor: {
            CallExpression(path) {
                const { node } = path;
                if (t.isMemberExpression(node.callee) &&
                node.callee.object.name === 'console' &&
                node.callee.property.name === 'log'
                ) {
                    path.remove();
                }
            },

        },
    };
}

將這個插件加入到你的babel插件列表中,能夠看到它真的生效了,一切都是這麼簡單。so amazing!

結語

開頭提到的經常使用庫prettire, eslint, css-loader 等等其實都是先生成AST,而後再操做AST,最後在生成代碼。只不過操做AST的過程很複雜,觸類旁通在項目裏,組件庫升級,組件批量替換均可以使用這個思路。甚至能夠根據業務作一些本身業務方的babel-plugin都行。
感謝您的閱讀,有問題能夠在評論區交流~

幫助連接
如何開發一個babel-plugin
《AST for JavaScript developers》
相關文章
相關標籤/搜索