Vue渲染流程步驟詳解
在 Vue 里渲染一塊內(nèi)容,會有以下步驟及流程:
第一步,解析語法,生成AST
第二步,根據(jù)AST結(jié)果,完成data數(shù)據(jù)初始化
第三步,根據(jù)AST結(jié)果和DATA數(shù)據(jù)綁定情況,生成虛擬DOM
第四步,將虛擬DOM 生成真正的DOM插入到頁面中,進行頁面渲染。
那怎么理解這個流程呢?
一、解析語法生成AST
AST 語法樹,實際就是抽象語法樹(Abstract Syntax Tree),是指通過構(gòu)建語法樹的形式將源代碼中的語句映射到樹中的每一個節(jié)點上。
DOM 結(jié)構(gòu)樹,也是AST中的一種,把HTML DOM語法解析并生成最終頁面。
我們詳細看看這個過程:
1、捕獲語法
在生成AST的過程中,會涉及到編譯器的原理, 會經(jīng)過以下過程:
(1)、語法分析
語法分析的任務(wù)是在詞法分析的基礎(chǔ)上將單詞序列組合成各類語法短語。如 :程序、語句、表達式等。語法分析程序判斷源程序在結(jié)構(gòu)上是否正確, 如 v-if` / v-for 這樣的指令 ,也有``這樣的自定義 DOM 標(biāo)簽,還有`click`/`props 這樣的簡化綁定語法。需要將它們一一解析出來,并相應(yīng)地進行后續(xù)處理。
(2)、語義分析
語義分析是審查源程序有無語義錯誤,為代碼生成階段收集類型信息,一般類型檢查也會在這個過程中進行。如我們綁定了某個不存在的變量或者事件,又或者是使用了某個未定義的自定義組件等,都會在這個階段進行報錯提示。
(3) 、生成 AST
在Vue 里,語法分析、語義分析基本上是通過正則的方式來處理,生成 AST其實就是將解析出來的元素、指令、屬性、父子節(jié)點關(guān)系等內(nèi)容進行處理,得到一個 AST 對象,以下是簡化后的源碼:
/**
* HTML編譯成AST對象
*/
export function parse(
template: string,
options: CompilerOptions
): ASTElement | void
{
// 返回AST對象
// 篇幅原因,一些前置定義省略
// 此處開始解析HTML模板
parseHTML(template, {
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
start(tag, attrs, unary) {
// 一些前置檢查和設(shè)置、兼容處理此處省略
// 此處定義了初始化的元素AST對象
const element: ASTElement = {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent: currentParent,
children: []
};
// 檢查元素標(biāo)簽是否合法(不是保留命名)
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
process.env.NODE_ENV !== "production" &&
warn(
"Templates should only be responsible for mapping the state to the " +
"UI. Avoid placing tags with side-effects in your templates, such as " +
`<${tag}>` +
", as they will not be parsed."
);
}
// 執(zhí)行一些前置的元素預(yù)處理
for (let i = 0; i < preTransforms.length; i++) {
preTransforms[i](element, options);
}
// 是否原生元素
if (inVPre) {
// 處理元素元素的一些屬性
processRawAttrs(element);
} else {
// 處理指令,此處包括v-for/v-if/v-once/key等等
processFor(element);
processIf(element);
processOnce(element);
processKey(element); // 刪除結(jié)構(gòu)屬性
// 確定這是否是一個簡單的元素
element.plain = !element.key && !attrs.length;
// 處理ref/slot/component等屬性
processRef(element);
processSlot(element);
processComponent(element);
for (let i = 0; i < transforms.length; i++) {
transforms[i](element, options);
}
processAttrs(element);
}
// 后面還有一些父子節(jié)點等處理,此處省略
}
// 其他省略
});
return root;
}2、DOM 元素捕獲
假如我們需要捕獲一個<div>元素,再生成一個<div>元素。
有一段模板,我們可以對它進行捕獲:
<div> <a>111</a> <p>222<span>333</span> </p> </div>
捕獲后我們可以得到這樣一個對象:
divObj = {
dom: {
type: "dom",
ele: "div",
nodeIndex: 0,
children: [
{
type: "dom",
ele: "a",
nodeIndex: 1,
children: [{ type: "text", value: "111" }]
},
{
type: "dom",
ele: "p",
nodeIndex: 2,
children: [
{ type: "text", value: "222" },
{
type: "dom",
ele: "span",
nodeIndex: 3,
children: [{ type: "text", value: "333" }]
}
]
}
]
}
};
這個對象保存了我們需要的一些信息:
HTML元素里需要綁定哪些變量,因為變量更新的時候需要更新該節(jié)點內(nèi)容。
以怎樣的方式來拼接,是否有邏輯指令,如v-if、v-for等
哪些節(jié)點綁定了什么監(jiān)聽事件,是否匹配一些常用的事件能力支持
Vue 會根據(jù) AST 對象生成一段可執(zhí)行的代碼,我們看看這部分的實現(xiàn):
// 生成一個元素
function genElement(el: ASTElement): string {
// 根據(jù)該元素是否有相關(guān)的指令、屬性語法對象,來進行對應(yīng)的代碼生成
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或者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, true);
code = `_c('${el.tag}'${
data ? `,${data}` : "" // data
}${
children ? `,${children}` : "" // children
})`;
}
// 模塊轉(zhuǎn)換
for (let i = 0; i < transforms.length; i++) {
code = transforms[i](el, code);
}
// 返回最后拼裝好的可執(zhí)行的代碼
return code;
}
}3、模板引擎賦能
通過以上介紹,或許大家會說,原本就是一個<div>,經(jīng)過 AST 生成一個對象,最終還是生成一個<div>,這不是多余的步驟嗎?
其實 ,在這個過程中我們可以實現(xiàn)一些功能:
排除無效 DOM 元素,并在構(gòu)建過程可進行報錯
使用自定義組件的時候,可匹配出來
可方便地實現(xiàn)數(shù)據(jù)綁定、事件綁定等功能
為虛擬 DOM Diff 過程打下鋪墊
HTML 轉(zhuǎn)義預(yù)防 XSS 漏洞
通用的模板引擎能處理很多低效又重復(fù)的工作,例如瀏覽器兼容、全局事件的統(tǒng)一管理和維護、模板更新的虛擬 DOM 機制、樹狀組織管理組件。這樣我們知道了模板引擎都做了什么事情后,就可以區(qū)分 Vue 框架提供的能力和我們需要自行處理的邏輯,可以更專注于業(yè)務(wù)開發(fā)。
二、虛擬DOM
虛擬 DOM 大概可分成三個過程:
第一步,用 JS 對象模擬 DOM 樹,得到一棵虛擬 DOM 樹。
第二步,當(dāng)頁面數(shù)據(jù)變更時,生成新的虛擬 DOM 樹,比較新舊兩棵虛擬 DOM 樹的差異。
第三步,把差異應(yīng)用到真正的 DOM 樹上。
1、用 JS 對象模擬 DOM 樹
為什么要用到虛擬 DOM ? 因為一個真正的 DOM 元素非常龐大,擁有很多的屬性值,而實際上我們并不是全部都會用到,通常包括節(jié)點內(nèi)容、元素位置、樣式、節(jié)點的添加刪除等方法。所以,我們通過用 JS 對象表示 DOM 元素的方式,可以大大降低了比較差異的計算量。
我們來看一下 VNode 源碼,只有以下20來個屬性:
tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context fordevtools fnScopeId: ?string; // functional scope id support
2 、比較新舊兩棵虛擬 DOM 樹的差異
虛擬 DOM 中,差異對比是很關(guān)鍵的一步,當(dāng)狀態(tài)變更的時候,重新構(gòu)造一棵新的對象樹。然后用新的樹和舊的樹進行比較,記錄兩棵樹差異。這樣的差異需要記錄:
需要替換掉原來的節(jié)點
移動、刪除、新增子節(jié)點
修改了節(jié)點的屬性
對于文本節(jié)點的文本內(nèi)容改變
下圖,我們對比兩棵 DOM 樹,得到的差異有:
p 元素插入了一個 span 元素子節(jié)點
原先的文本節(jié)點挪到了 span 元素子節(jié)點下面

3、應(yīng)用差異到真正的 DOM 樹
通過前面的示例,我們知道差異記錄要應(yīng)用到真正的 DOM 樹上,需要進行一些操作,例如節(jié)點的替換、移動、刪除,文本內(nèi)容的改變等。
在 Vue 中是怎么進行 DOM Diff 呢? 簡單看這段代碼感受下, 雖然代碼里很多函數(shù)沒貼出來,但其實看函數(shù)名也可以大概理解都是什么作用,例如updateChildren、addVnodes、removeVnodes、setTextContent等。
// 對比差異后更新
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== "production") {
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}三、數(shù)據(jù)綁定
在 Vue 中,最基礎(chǔ)的模板語法是數(shù)據(jù)綁定。
例如:
<div>{{ message }}</div>最終頁面展示內(nèi)容為<div>test</div>。那這是怎么做到的呢?
1、 數(shù)據(jù)綁定的實現(xiàn)
這種使用雙大括號來綁定變量的方式,我們稱之為數(shù)據(jù)綁定。
數(shù)據(jù)綁定的過程其實不復(fù)雜:
(1) 、解析語法生成 AST
(2) 、根據(jù) AST 結(jié)果生成 DOM
(3) 、將數(shù)據(jù)綁定更新至模板
這個過程是 Vue 中模板引擎在做的事情,我們來看看上面在 Vue 里的代碼片段<div></div>,我們可以通過 DOM 元素捕獲,解析后獲得這樣一個 AST 對象:
divObj = {
dom: {
type: "dom",
ele: "div",
nodeIndex: 0,
children: [{ type: "text", value: "" }]
},
binding: [{ type: "dom", nodeIndex: 0, valueName: "message" }]
};我們在生成 DOM 的時候,添加對message的監(jiān)聽,數(shù)據(jù)更新時會找到對應(yīng)的nodeIndex更新值:
// 假設(shè)這是一個生成 DOM 的過程,包括 innerHTML 和事件監(jiān)聽
function generateDOM(astObject) {
const { dom, binding = [] } = astObject;
// 生成DOM,這里假設(shè)當(dāng)前節(jié)點是baseDom
baseDom.innerHTML = getDOMString(dom);
// 對于數(shù)據(jù)綁定的,來進行監(jiān)聽更新
baseDom.addEventListener("data:change", (name, value) => {
// 尋找匹配的數(shù)據(jù)綁定
const obj = binding.find(x => x.valueName == name);
// 若找到值綁定的對應(yīng)節(jié)點,則更新其值。
if (obj) {
baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;
}
});
}
// 獲取DOM字符串,這里簡單拼成字符串
function getDOMString(domObj) {
// 無效對象返回''
if (!domObj) return "";
const { type, children = [], nodeIndex, ele, value } = domObj;
if (type == "dom") {
// 若有子對象,遞歸返回生成的字符串拼接
const childString = "";
children.forEach(x => {
childString += getDOMString(x);
});
// dom對象,拼接生成對象字符串
return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;
} else if (type == "text") {
// 若為textNode,返回text的值
return value;
}
}這樣,我們就能在message變量更新的時候,通過該變量關(guān)聯(lián)的引用,來自動更新對應(yīng)展示的內(nèi)容。而要知道message變量什么時候進行了改變,我們需要對數(shù)據(jù)進行監(jiān)聽。
2、數(shù)據(jù)更新監(jiān)聽
加粗樣式
我們能看到,上面的簡單代碼描述過程中,使用的數(shù)據(jù)監(jiān)聽方法是用了addEventListener("data:change", Function)的方式。
在 Vue 中,數(shù)據(jù)更新的時候就執(zhí)行了模板更新、watch、computed 等一些工作,主要是依賴了Getter/Setter。而 Vue3.0 將使用Proxy的方式來進行:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// getter
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
// setter最終更新后會通知
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});Vue 中大多數(shù)能力都依賴于模板引擎,包括組件化管理、事件管理、Vue 實例、生命周期等,相信只要理解了 AST、虛擬 DOM、數(shù)據(jù)綁定相關(guān)的機制后,再去翻閱 Vue 源碼 ,了解更多的能力就不是問題了。
以上就是Vue渲染流程步驟詳解的詳細內(nèi)容,更多關(guān)于Vue渲染的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue使用Google Recaptcha驗證的實現(xiàn)示例
我們最近的項目中需要使用谷歌機器人驗證,所以就動手實現(xiàn)一下,本文就來詳細的介紹一下vue Google Recaptcha驗證,感興趣的可以了解一下2021-08-08
讓webpack+vue-cil項目不再自動打開瀏覽器的方法
今天小編就為大家分享一篇讓webpack+vue-cil項目不再自動打開瀏覽器的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09
關(guān)于element-ui中el-form自定義驗證(調(diào)用后端接口)
這篇文章主要介紹了關(guān)于element-ui中el-form自定義驗證(調(diào)用后端接口),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07

