【js】JavaScript parser實現淺析

  最近筆者的團隊遷移了webpack2,在遷移過程當中,筆者發現webpack2中有至關多的兼容代碼,雖然外界有不少聲音一直在質疑做者爲何要破壞性更新,其實你們也都知道webpack1那種過於「靈活」的配置方式是有待商榷的,因此做者纔會在webpack2上進行了不少規範,可是,筆者卻隱隱的以爲,等到webpack3的時候,估計會有更多的破壞性更新,否則也不會有這個webpack2了。因而心中有關webpack的話題便也擱置了,且等它更穩定一些,再談不遲,今天先來說講在劇烈的版本變化中,不變的部分。前端

  你們都知道,webpack是作模塊綁定用的,那麼就不得不牽涉到語法解析的內容,並且其極高的擴展性,也每每須要依賴於語法解析,而在webpack內部使用acorn作語法解析,相似的還有babel使用的babylon,今天就帶來二者的簡要分析。node

  官方給二者的定義都叫作JavaScript parser,內部也一致的使用了AST(Abstract syntax tree,即抽象語法樹)的概念。若是對這個概念不明白的同窗能夠參考WIKIAST的解釋webpack

  由於babylon引用了flow,eslint等一些checker,因此整個項目結構至關的規範,筆者僅已7.0.0爲例:git

  文件夾目錄以下:github

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

  大概流程是這樣的:web

一、首先調用index.js的parse;
二、實例化一個parser對象,調用parser對象的parse方法,開始轉換;
三、初始化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 ,將其參數歸類
四、迭代過程完成後,封閉節點,完成body閉合express

  不過在筆者看來,babylon的parser實現彷佛並不能稱得上是一個很好的實現,而實現中每每還會使用的forward declaration(相似虛函數的概念),以下圖babel

  

  一個「+」在方法前面的感受就像是要之前的IIFE同樣。。ide

  有點扯遠了,總的來講依然是傳統語法分析的幾個步驟,不過筆者在讀源碼的時候一直以爲蠻奇怪的爲什麼他們內部要使用繼承來實現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的抽象,主要是用於提供一些匹配換行符的正則,一般都想到的是:

/\r\n?|\n/

但實際上完整的倒是

/\r\n?|\n|\u2028|\u2029/

並且考慮到ASCII碼的狀況,還須要很糾結的枚舉出非空格的狀況

/[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/

  由於parse處理的是咱們實際開發中本身coding的代碼,不一樣的人不一樣的風格,會有怎麼樣的奇怪的方式實際上是很是考驗完備性思惟的一項工做,並且這每每比咱們平常的業務工做的場景更爲複雜,它不少時候甚至是接近一個可能性的全集,而並不是「大機率可能」的一個集合。雖然咱們平常工做這種parser幾乎是透明的,咱們在init的前端項目時基本已經部署好了開發環境,可是對於某些狀況下的實際問題定位,卻又有非凡的意義,並且,這還在必定時間內是一個常態,雖然可能在不久的將來,就會有更加智能更增強大的前端IDE。

  有關ast的實驗,能夠試一下這個網站:https://astexplorer.net/

相關文章
相關標籤/搜索