Vue 的編譯器模塊相對獨立且簡單,本篇就從這塊入手,先把它干掉。
編譯器代碼入口文件
前面已經提到,Vue 項目中的 entry-runtime.js 文件是 Vue 用於構建 僅包含運行時 的源碼文件,而 entry-runtime-with-compiler.js 是用於構建 同時包含編譯器和運行時 的全功能文件。因此兩個文件的差集必然就是編譯器實現。
先看一下 entry-runtime.js 文件的內容:
import Vue from './runtime/index'
export default Vue
文件里總共就這兩行代碼。這樣的話就基本確定編譯器相關的代碼就在 entry-runtime-with-compiler.js 文件里了,事實證明也確實是這樣。
Vue.prototype.$mount
entry-runtime-with-compiler.js 文件里的關鍵代碼是為 Vue 的 prototype 擴展了一個 $mount
方法,並將模板編譯相關的工作都封裝在了這個 $mount 方法里。
在具體深扒 $mount
方法的內部實現之前,有必要先看一下它的應用場景是怎樣的,這樣會更有助於理解它內部是怎么工作的。
例如下面一段 html 模板:
<div id="index">
<div>{{msg}}</div>
</div>
開發者可以通過如下操作使用 Vue 將上面這段模板編譯成 render 函數:
let vm = new Vue({
data: {
msg: 'hello',
}
});
// 實例化 Vue 時 new Vue(options) 傳入的 options 可通過 vm.$options 訪問
console.log(vm.$options.render);
/* Console 輸出:
* undefined
*/
vm.$mount('#index');
console.log(vm.$options.render);
/* Console 輸出:
* ƒ anonymous() {
* with(this){return _c('div',{attrs:{"id":"index"}},[_c('div',[_v(_s(msg))])])}
* }
*/
可以看到在調用 $mount
方法之后已經生成了 Vue 的 render 函數。
更常用也更方便的用法是:
new Vue({
el: '#index',
data: {
msg: 'hello',
},
});
這兩種寫法是完全等價的。實際上,如果在實例化 Vue 的時候提供了 el 選項,Vue 也是在內部調用 $mount
方法進行編譯的。
接下來就看看 $mount
方法的具體是怎么實現的,為了更加清晰地描述思路,以下均使用偽代碼進行書寫:
/**
* 作用:將 Vue 的 html 模板編譯成 render 函數。
*
* 通過將 $mount 方法定義在 Vue 的 prototype 上,
* 使得每一個 new 出來的 Vue 實例都能使用 $mount 方法。
*/
Vue.prototype.$mount = function (el){
// options 是 new Vue(options) 提供的實參 options
const options = this.$options;
// 優先使用實例化 Vue 時提供 render 函數
if (options.render) {
// 已經是 render 函數了,因此不用做任何操作
return this;
// 如果沒有提供 render 函數,則優先使用提供的 template 選項
}else if(options.template){
template = getOuterHTML(options.template);
// 如果既沒有提供 render 函數,又沒有 template 選項,就使用 el 選項
}else{
template = getOuterHTML(el);
}
// 編譯 html 模板生成 render 函數,並賦給 options 的 render 選項
// 這也是為什么上面在調用 $mount 方法之后 vm.$options.render 的值發生了變化
options.render = compileToFunctions(template);
return this;
}
// 負責兼容多樣化的輸入形式並返回要處理的 html模板片段
function getOuterHTML(){/*...*/}
// 負責將 html模板片段編譯成 render 函數
function compileToFunctions(el){/*...*/}
可以看到,如果實例化 Vue 的時候同時提供了 render、template、el 選項中的多個,則 Vue 使用的優先級是 render > template > el。
# getOuterHTML 函數
上面的 getOuterHTML
函數所做的工作就是兼容你使用 Vue 的各種姿勢,比如:
{ el: '#index' }
{ el: document.querySelector('#index') }
{ template: '#index' }
{ template: '<div>{{msg}}</div>'}
你可以傳 CSS 選擇器,也可以直接傳 DOM, 還可以傳 html 片段,怎么玩你說了算。getOuterHTML
函數的返回值是 DOM 的 outerHTML,總之,它負責得到 html 模板片段。
至此一切仍然是在扯淡,上面的都只是前戲,現在還沒進入真正的編譯階段。眼賊的同學估計已經看到了,上面的 compileToFunctions 函數才是真刀實槍負責編譯的。
# compileToFunctions 函數
接下來就扒進去看看 compileToFunctions
是怎么把 getOuterHTML
獲得的 html 模板片段編譯成 render 函數的。
compileToFunctions
函數編譯模板的過程主要分為三步:
- 將 html 模板解析成抽象語法樹(AST)。
- 對 AST 做優化處理。
- 根據 AST 生成 render 函數。
什么是抽象語法樹
抽象語法樹(Abstract Syntax Tree) 是源代碼語法結構的抽象表示,並以樹這種數據結構進行描述。AST 屬編譯原理范疇,有比較成熟的理論基礎,因此被廣泛運用在對各種程序語言(JavaScript, C, Java, Python等等)的編譯處理中。Vue 同樣也是使用 AST 作為中間形式完成對 html 模板的編譯。
構建 AST 的一般過程
首先看一下第一步,也就是 解析成 AST。但是在繼續 Vue 模板如何生成 AST 之前,有必要先看一下 AST 的一般解析過程。
通常程序語言解析成 AST 的過程會分為兩步:
- 詞法分析(Lexical Analysis)
- 語法分析(Syntax Analysis)
拿咱最熟悉的 JavaScript 來說吧,比如下面一段程序:
let a = 1
詞法分析器會把代碼的字符序列轉換為單詞序列(tokens)。經過詞法分析后就能得到如下一個詞素列表:
[
{ type: 'Keyword', value: 'let' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator', value: '=' },
{ type: 'Numeric', value: '1' }
]
語法分析器會在詞法分析的基礎上將單詞序列(tokens)組合成各類語法短語(語句、表達式等)。經過語法分析后即可得到 AST 的 JSON 格式:
{
type: "Program",
body: [
{
type: "VariableDeclaration",
declarations: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "a"
},
init: {
type: "Literal",
value: 1,
raw: "1"
}
}
],
kind: "let"
}
],
sourceType: "script"
}
上面的英文單詞大家不認識的自己去搜下翻譯哈。JSON 是天然的樹形結構,樹形圖想必諸位早就腦補出來了吧:
源代碼生成的抽象語法樹
以上是使用 Esprima 工具對 JS 代碼進行詞法分析和語法分析的結果。
這里有一個 在線的AST生成工具。
還有一個 AST樹形圖預覽工具。
Vue 構建的 AST
扯了這么多,應該對抽象語法樹有個模糊的概念了吧,這對理解 Vue 的 AST 構建過程就足夠用了。
回到正題,Vue 的 html 模板比較特殊,因為它根本算不上是一門語言,而是基於 HTML 的聲明式綁定。因此,Vue 生成的 AST 類似於大家已經非常熟悉且非常成熟的 DOM 樹,實際上 Vue 也確實是仿照着 DOM 樹進行解析的。只要你熟悉 DOM 樹,Vue 生成的 AST 是灰常好看且簡單的。如果連 DOM 樹都不了解,那咱只能幫你到這里了,你一定是個假前端。
最后再次強調的一點是,Vue 編譯器的編譯結果是一個函數——Vue 的 render 函數,AST 只是方便處理的中間形式。
本篇完,將在下篇深究 Vue 構建 AST 的細節。
本系列會以每周一篇的速度持續更新,喜歡的小伙伴記得點關注哦。