前言
上一篇文章 Vue 原始碼解讀(8)—— 編譯器 之 解析 詳細詳解了編譯器的第一部分,如何將 html 模版字串編譯成 AST。今天帶來編譯器的第二部分,優化 AST,也是大家常說的靜態標記。
目標
深入理解編譯器的靜態標記過程
原始碼解讀
入口
/src/compiler/index.js
/**
* 在這之前做的所有的事情,只有一個目的,就是為了構建平臺特有的編譯選項(options),比如 web 平臺
*
* 1、將 html 模版解析成 ast
* 2、對 ast 樹進行靜態標記
* 3、將 ast 生成渲染函式
* 靜態渲染函式放到 code.staticRenderFns 陣列中
* code.render 為動態渲染函式
* 在將來渲染時執行渲染函式得到 vnode
*/
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 將模版解析為 AST,每個節點的 ast 物件上都設定了元素的所有資訊,比如,標籤資訊、屬性資訊、插槽資訊、父節點、子節點等。
// 具體有那些屬性,檢視 start 和 end 這兩個處理開始和結束標籤的方法
const ast = parse(template.trim(), options)
// 優化,遍歷 AST,為每個節點做靜態標記
// 標記每個節點是否為靜態節點,然後進一步標記出靜態根節點
// 這樣在後續更新的過程中就可以跳過這些靜態節點了
// 標記靜態根,用於生成渲染函式階段,生成靜態根節點的渲染函式
if (options.optimize !== false) {
optimize(ast, options)
}
// 從 AST 生成渲染函式,生成像這樣的程式碼,比如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)"
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
optimize
/src/compiler/optimizer.js
/**
* 優化:
* 遍歷 AST,標記每個節點是靜態節點還是動態節點,然後標記靜態根節點
* 這樣在後續更新的過程中就不需要再關注這些節點
*/
export function optimize(root: ?ASTElement, options: CompilerOptions) {
if (!root) return
/**
* options.staticKeys = 'staticClass,staticStyle'
* isStaticKey = function(val) { return map[val] }
*/
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 平臺保留標籤
isPlatformReservedTag = options.isReservedTag || no
// 遍歷所有節點,給每個節點設定 static 屬性,標識其是否為靜態節點
markStatic(root)
// 進一步標記靜態根,一個節點要成為靜態根節點,需要具體以下條件:
// 節點本身是靜態節點,而且有子節點,而且子節點不只是一個文字節點,則標記為靜態根
// 靜態根節點不能只有靜態文字的子節點,因為這樣收益太低,這種情況下始終更新它就好了
markStaticRoots(root, false)
}
markStatic
/src/compiler/optimizer.js
/**
* 在所有節點上設定 static 屬性,用來標識是否為靜態節點
* 注意:如果有子節點為動態節點,則父節點也被認為是動態節點
* @param {*} node
* @returns
*/
function markStatic(node: ASTNode) {
// 通過 node.static 來標識節點是否為 靜態節點
node.static = isStatic(node)
if (node.type === 1) {
/**
* 不要將元件的插槽內容設定為靜態節點,這樣可以避免:
* 1、元件不能改變插槽節點
* 2、靜態插槽內容在熱過載時失敗
*/
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
// 遞迴終止條件,如果節點不是平臺保留標籤 && 也不是 slot 標籤 && 也不是內聯模版,則直接結束
return
}
// 遍歷子節點,遞迴呼叫 markStatic 來標記這些子節點的 static 屬性
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
// 如果子節點是非靜態節點,則將父節點更新為非靜態節點
if (!child.static) {
node.static = false
}
}
// 如果節點存在 v-if、v-else-if、v-else 這些指令,則依次標記 block 中節點的 static
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
isStatic
/src/compiler/optimizer.js
/**
* 判斷節點是否為靜態節點:
* 通過自定義的 node.type 來判斷,2: 表示式 => 動態,3: 文字 => 靜態
* 凡是有 v-bind、v-if、v-for 等指令的都屬於動態節點
* 元件為動態節點
* 父節點為含有 v-for 指令的 template 標籤,則為動態節點
* @param {*} node
* @returns boolean
*/
function isStatic(node: ASTNode): boolean {
if (node.type === 2) { // expression
// 比如:{{ msg }}
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
markStaticRoots
/src/compiler/optimizer.js
/**
* 進一步標記靜態根,一個節點要成為靜態根節點,需要具體以下條件:
* 節點本身是靜態節點,而且有子節點,而且子節點不只是一個文字節點,則標記為靜態根
* 靜態根節點不能只有靜態文字的子節點,因為這樣收益太低,這種情況下始終更新它就好了
*
* @param { ASTElement } node 當前節點
* @param { boolean } isInFor 當前節點是否被包裹在 v-for 指令所在的節點內
*/
function markStaticRoots(node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
// 節點是靜態的 或者 節點上有 v-once 指令,標記 node.staticInFor = true or false
node.staticInFor = isInFor
}
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
// 節點本身是靜態節點,而且有子節點,而且子節點不只是一個文字節點,則標記為靜態根 => node.staticRoot = true,否則為非靜態根
node.staticRoot = true
return
} else {
node.staticRoot = false
}
// 當前節點不是靜態根節點的時候,遞迴遍歷其子節點,標記靜態根
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
// 如果節點存在 v-if、v-else-if、v-else 指令,則為 block 節點標記靜態根
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
總結
-
面試官 問:簡單說一下 Vue 的編譯器都做了什麼?
答:
Vue 的編譯器做了三件事情:
-
將元件的 html 模版解析成 AST 物件
-
優化,遍歷 AST,為每個節點做靜態標記,標記其是否為靜態節點,然後進一步標記出靜態根節點,這樣在後續更新的過程中就可以跳過這些靜態節點了;標記靜態根用於生成渲染函式階段,生成靜態根節點的渲染函式
-
從 AST 生成執行渲染函式,即大家說的 render,其實還有一個,就是 staticRenderFns 陣列,裡面存放了所有的靜態節點的渲染函式
-
-
面試官:詳細說一下靜態標記的過程
答:
-
標記靜態節點
-
通過遞迴的方式標記所有的元素節點
-
如果節點本身是靜態節點,但是存在非靜態的子節點,則將節點修改為非靜態節點
-
-
標記靜態根節點,基於靜態節點,進一步標記靜態根節點
-
如果節點本身是靜態節點 && 而且有子節點 && 子節點不全是文字節點,則標記為靜態根節點
-
如果節點本身不是靜態根節點,則遞迴的遍歷所有子節點,在子節點中標記靜態根
-
-
-
面試官:什麼樣的節點才可以被標記為靜態節點?
答:
-
文字節點
-
節點上沒有 v-bind、v-for、v-if 等指令
-
非元件
-
連結
- 配套視訊,微信公眾號回覆:"精通 Vue 技術棧原始碼原理視訊版" 獲取
- 精通 Vue 技術棧原始碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
感謝各位的:關注、點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。 感謝各位的 關注、 點贊、收藏和評論。
新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。