本人上一篇博文裏分析了一下Vue中的模板編譯器的原理及其實現方式(如果感興趣可以去看一下:Vue源碼學習之模板編譯器原理),如果把Vue看成一輛跑車,模板編譯器可以看做是跑車的底盤,那麼優化器便可以看做是一種更爲輕便的合金材料,跑車沒有這種材料,依舊能夠正常行駛,但如果將跑車的底盤換成這種材料,便可以使得整個車身更加輕盈,跑得更快。優化器起到的便是通過將生成的抽象語法樹中的一些靜態節點(所謂靜態節點,就是第一次渲染到頁面上之後,就不會隨着狀態的改變而改變的節點)標記出來,在渲染階段時,這些節點只需要首次渲染,以後無論是狀態發生怎樣的變化,他都不會變,減少了很多沒有必要的diff判斷和dom更新的操作,使得vue運行的效率更高。
上面說了,優化器的原理其實就是標記靜態節點,那麼,我們要怎麼判斷哪些節點是靜態節點,又要怎麼去標記他呢?具體想看一下代碼實現,其中有詳細的註釋解釋,就不再贅述了:
// compiler/optimize.js 優化器,對通過解析器生成的抽象語法樹中的靜態節點和靜態根節點進行標記,在渲染階段便可以跳過這些渲染節點進行渲染,以提升效率
import {AST_ITEM_TYPE} from "../shared/constants.js";
import {cached, isBuiltInTag, isReservedTag, makeMap} from "../shared/utils.js";
const genStaticKeysCached = cached(genStaticKeys);
let isStaticKey;
export const optimize = root => {
// 將所有的靜態屬性都羅列出來,方便區分動態屬性
isStaticKey = genStaticKeysCached('');
// 標記所有靜態節點
markStatic(root);
// 標記所有靜態根節點
markStaticRoots(root, false);
};
/**
* 標記所有靜態節點
* @param node
*/
export const markStatic = node => {
// 標記當前節點是不是靜態標籤
node.static = isStatic(node);
if (node.type === AST_ITEM_TYPE.ELEMENT) {// 1-元素節點 2-帶變量的文本節點 3-不帶變量的文本節點
if (
!isReservedTag(node.tag) && //如果不是網頁保留標籤
node.tag !== 'slot' && // 不是一個插槽
node.attrsMap['inline-template'] === null // 不是一個內聯模板
) {
return;
}
// 循環標記所有子節點,並適時修正當前節點
let children = node.children;
for (let i = 0, len = children.length; i < len; i++) {
let child = children[i];
markStatic(child);
//若子節點不是靜態節點,那麼其父級節點不可能是靜態節點
if (!child.static) {
node.static = false;
}
}
// 如果當前節點綁定了v-if
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;
}
}
}
}
};
/**
* 標記所有靜態根節點
* @param node
* @param isInFor 是否在循環體重
*/
export const markStaticRoots = (node, isInFor) => {
if (node.type === AST_ITEM_TYPE.ELEMENT) {// 1-元素節點 2-帶變量的文本節點 3-不帶變量的文本節點
// 如果當前節點是靜態節點或者當前節點標記爲只渲染一次,那麼標記一下改節點在for循環中是否是靜態的
if (node.static || node.once) {
node.staticInFor = isInFor;
}
if (
node.static &&//當前節點是靜態節點
node.children.length &&//當前節點擁有子節點
!(node.children.length === 1 && node.children[0].type === AST_ITEM_TYPE.TEXT)//排除當前節點的子節點只有一個並且該節點是不帶變量的文本節點,因爲這種情況優化的成本大於收益,而我們的優化器的目的就是要降低成本
) {
node.staticRoot = true;
return;
} else {
node.staticRoot = false;
}
if (node.children) {
//循環遞歸調用,標記當前節點下的所有子節點中滿足條件的靜態根節點
node.children.forEach(child => markStaticRoots(child, isInFor || !!child.for));
}
// 如果當前節點綁定了v-if
if (node.ifConditions) {
// 將除了他自己之外的條件鏈節點都進行以下標記
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor);
}
}
}
};
/**
* 判斷目標節點是否爲靜態節點
* @param node
* @returns {boolean}
*/
export const isStatic = node => {
// 帶變量的文本節點
if (node.type === AST_ITEM_TYPE.EXPRESSION) {
return false;
}
// 不帶變量的文本節點
if (node.type === AST_ITEM_TYPE.TEXT) {
return true;
}
return !!(
node.pre || // 如果帶有v-pre的標籤必定是靜態標籤
(
!node.hasBindings && // 沒有動態綁定的屬性
!node.if && !node.for && // 沒有 v-if 、 v-for 、 v-else
!isBuiltInTag(node.tag) && // 不是一個vue內置標籤
isReservedTag(node.tag) && // 是html或svg的保留標籤
!isDirectChildOfTemplateFor(node) && // 不是循環template的直接子節點
Object.keys(node).every(isStaticKey) // 含有指定屬性
)
)
};
/**
* 是否是循環template的直接子節點
* @param node
* @returns {boolean}
*/
function isDirectChildOfTemplateFor (node) {
while (node.parent) {
node = node.parent;
if (node.tag !== 'template') {
return false
}
if (node.for) {
return true
}
}
return false
}
/**
* 獲取靜態key值
* @param keys
* @returns {function(*): *}
*/
function genStaticKeys (keys) {
return makeMap(
'type,tag,attrList,attrsMap,plain,parent,children,attrs,startIndex,endIndex,rawAttrsMap' +
(keys ? ',' + keys : '')
)
}
從上面的源碼可以看出,其實優化器只是做了兩件事
1、找出靜態節點並標記上static=true
2、找出靜態根節點並標記上staticRoot=true
當然,只是打上這個標記的話,並沒有什麼太大的作用,真正起到優化作用的是在patching階段根據這兩個標記,可以繞過這些被標記的靜態節點,這樣就會節省了很多工作量,至於patching內部是如何判斷的,不是本文研究範疇,這裏就先不細說,之後整理出patching的文章後在裏面在詳細分析。