本來以為 Vue 的編譯器模塊比較好欺負,結果發現並沒有那麼簡單。每一種語法指令都要考慮到,處理起來相當複雜。上篇已經生成了 AST,本篇依然對 Vue 源碼做簡化處理,探究 Vue 是如果根據 AST 生成所需要的 render 函數的。 ...
閱讀目錄
本來以為 Vue 的編譯器模塊比較好欺負,結果發現並沒有那麼簡單。每一種語法指令都要考慮到,處理起來相當複雜。上篇已經生成了 AST,本篇依然對 Vue 源碼做簡化處理,探究 Vue 是如果根據 AST 生成所需要的 render 函數的。
優化 AST
優化 AST 的目的是優化整體性能,避免不必要計算。比如那些不存在數據綁定的節點,即純靜態的(purely static)在更新視圖時根本不需要改變,因此在數據批處理,頁面重渲染時可直接跳過它們。
Vue 通過遍歷 AST 找出內容為純靜態的節點並將其標記為 static:
function optimize (root) {
//-- 第一步 標記 AST 所有靜態節點 --
markStatic(root)
//-- 第二步 標記 AST 所有父節點(即子樹根節點) --
markStaticRoots(root, false)
}
首先標記所有靜態節點:
function markStatic (node) {
// 標記
if (node.type === 2) { // 插值表達式
node.static = false
}
if (node.type === 3) { // 普通文本
node.static = true
}
if (node.type === 1) { // 元素
// 如果所有子節點均是 static,則該節點也是 static
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
}
}
ASTNode 的 type 欄位用於標識節點的類型,可查看上一篇的 AST 節點定義:type
為 1
表示元素,type
為 2
表示插值表達式,type
為 3
表示普通文本。可以看到,在標記 ASTElement 時會依次檢查所有子元素節點的靜態標記,從而得出該元素是否為 static。
上面 markStatic
函數使用的是樹形數據結構的深度優先遍歷演算法,使用遞歸實現。
接下來標記出靜態子樹:
function markStaticRoots (node) {
if (node.type === 1) {
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
for (let i = 0; i < node.children.length; i++) {
markStaticRoots(node.children[i])
}
}
}
markStaticRoots
函數里並沒有什麼特別的地方,僅僅是對靜態節點又做了一層篩選。請註意函數中的那幾行註釋:
For a node to qualify as a static root, it should have children that are not just static text. Otherwise the cost of hoisting out will outweigh the benefits and it's better off to just always render it fresh.
翻譯過來大概意思是:一個節點如果想要成為靜態根,它的子節點不能單純只是靜態文本。否則,把它單獨提取出來還不如重渲染時總是更新它性能高。
這也是為什麼要在標記了所有 AST 節點之後又要標記一遍靜態子樹根。
生成 render 函數
不瞭解 render 函數的可以先看一下 Vue 的 render 函數。不瞭解也沒關係,就把它當成 Vue 最終需要的一段特定字元串拼接就行了。
function generate (el) {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el)
} else if (el.once && !el.onceProcessed) {
return genOnce(el)
} else if (el.for && !el.forProcessed) {
return genFor(el)
} else if (el.if && !el.ifProcessed) {
return genIf(el)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el)
} else {
const data = el.plain ? undefined : genData(el)
const children = el.inlineTemplate ? null : genChildren(el)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
return code
}
}
上面生成 render 函數的過程比較繁瑣,需要對不同情況作單獨處理,這裡不再一一展開。感興趣的可在 Vue 項目的 src/compiler/codegen/index.js 文件里仔細研究一下。
現在結合生成 AST 的方法就可以將編譯器初探中遺留的 compileToFunctions
方法定義出來了:
function compileToFunctions(el){
let ast = parseHTML(el)
optimize(ast)
return generate(ast)
}
也就是之前說的三步走:
- 將 html 模板解析成抽象語法樹(AST)。
- 對 AST 做優化處理。
- 根據 AST 生成 render 函數。
小結
Vue 首先根據使用者的傳參獲得待編譯的模板片段,然後使用正則匹配對片段里的標簽各個擊破,用步步蠶食的方法將整塊模板片段最終解析成一棵 AST。獲得 AST 後,後續的處理就非常方便了。Vue 會首先優化這棵 AST,將其中的靜態子樹找出來,這些靜態節點在之後的視圖更新和數據計算中是可以忽略掉的,從而提高性能。最後 Vue 遍歷這棵 AST 的每個節點,根據節點的類型生成不同的函數碎片,最後拼接成整個 render 函數。
值得一提的是,將 AST 作為編譯的中間形式是非常方便的,當 AST 構建出來之後,使用樹形結構的深度優先遍歷演算法就可以方便地對樹的每一個節點做處理。Vue 最後生成 render 函數也是通過遍歷 AST ,根據每個節點生成函數的一小部分,最後拼接成整個函數。
本篇完,Vue 編譯器部分到此完結。將在下篇開始進行 Vue 運行時相關的代碼剖析,運行時這部分代碼應該會是非常有意思的,包括 Vue 對象實例化,雙向綁定,虛擬 DOM 等內容。
本系列會以每周一篇的速度持續更新,喜歡的小伙伴記得點關註哦。