var VNode = function VNode (
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined;
this.context = context;
this.fnContext = undefined;
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.raw = false;
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false;
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
};
複製代碼
vnode其實就是一個描述節點的對象,描述如何建立真實的DOM節點;vnode的做用就是新舊vnode進行對比,只更新發生變化的節點。 VNode有註釋節點、文本節點、元素節點、組件節點、函數式組件、克隆節點:vue
var createEmptyVNode = function (text) {
if ( text === void 0 ) text = '';
var node = new VNode();
node.text = text;
node.isComment = true;
return node
};
複製代碼
只有isComment和text屬性有效,其他的默認爲false或者nullnode
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
複製代碼
只有一個text屬性數組
function cloneVNode (vnode) {
var cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
);
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.fnContext = vnode.fnContext;
cloned.fnOptions = vnode.fnOptions;
cloned.fnScopeId = vnode.fnScopeId;
cloned.asyncMeta = vnode.asyncMeta;
cloned.isCloned = true;
return cloned
}
複製代碼
克隆節點將vnode的全部屬性賦值到clone節點,而且設置isCloned = true,它的做用是優化靜態節點和插槽節點。以靜態節點爲例,由於靜態節點的內容是不會改變的,當它首次生成虛擬DOM節點後,再次更新時是不須要再次生成vnode,而是將原vnode克隆一份進行渲染,這樣在必定程度上提高了性能。緩存
{
children: [VNode, VNode],
context: {...},
tag: 'div',
data: {attr: {id: app}}
}
複製代碼
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
複製代碼
(2) componentInstance: 組件的實例,也是Vue的實例 對應的vnodebash
new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
)
複製代碼
即app
{
componentOptions: {},
componentInstance: {},
tag: 'vue-component-1-child',
data: {...},
...
}
複製代碼
虛擬DOM最重要的功能是patch,將VNode渲染爲真實的DOM。框架
patch中文意思是打補丁,也就是在原有的基礎上修改DOM節點,也能夠說是渲染視圖。DOM節點的修改有三種:async
當oldvnode中不存在,而vnode中存在時,就須要使用vnode新生成真實的DOM節點並插入到視圖中。首先若是vnode具備tag屬性,則認爲它是元素屬性,再根據當前環境建立真實的元素節點,元素建立後將它插入到指定的父節點。以上節生成的VNode爲例,首次執行函數
vm._update(vm._render(), hydrating);
複製代碼
vm._render()爲上篇生成的VNode,_update函數具體爲性能
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
// 緩存vnode
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 第一次渲染,preVnode是不存在的
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook. }; 複製代碼
因第一次渲染,執行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
,注意第一個參數是oldVnode爲vm.$el
爲元素節點,__patch__函數具體過程爲: (1) 先判斷oldVnode是否存在,不存在就建立vnode
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
}
複製代碼
(2) 存在進入else,判斷oldVnode是不是元素節點,若是oldVnode是元素節點,則
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode);
}
複製代碼
建立一個oldVnode節點,其形式爲
{
asyncFactory: undefined,
asyncMeta: undefined,
children: [],
componentInstance: undefined,
componentOptions: undefined,
context: undefined,
data: {},
elm: div#app,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "div",
text: undefined,
child: undefined
}
複製代碼
而後獲取oldVnode的元素節點以及其父節點,並建立新的節點
// replacing existing element
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
複製代碼
建立新節點的過程
// 標記是不是根節點
vnode.isRootInsert = !nested; // for transition enter check
// 這個函數若是vnode有componentInstance屬性,會建立子組件,後續具體介紹,不然不作處理
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
複製代碼
接着在對子節點處理
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
}
}
複製代碼
將vnode的屬性設置爲建立元素節點elem,建立子節點 createChildren(vnode, children, insertedVnodeQueue);
該函數遍歷子節點children數組
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
// 若是vnode是文本直接掛載
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
複製代碼
遍歷children,遞歸createElm方法建立子元素節點
else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
複製代碼
若是是評論節點,直接建立評論節點,並將其插入到父節點上,其餘的建立文本節點,並將其插入到父節點parentElm(剛建立的div)上去。 觸發鉤子,更新節點屬性,將其插入到parentElm('#app'元素節點)上
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
複製代碼
最後將老的節點刪掉
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
複製代碼
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
var i;
var listeners = cbs.remove.length + 1;
...
// recursively invoke hooks on child component root node
if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm);
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm);
}
if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
i(vnode, rm);
} else {
// 刪除id爲app的老節點
rm();
}
} else {
removeNode(vnode.elm);
}
}
複製代碼
初次渲染結束。
爲了更好地測試,模板選用
<div id="app">{{ message }}<button @click="update">更新</button></div>
複製代碼
點擊按鈕,會更新message,從新渲染視圖,生成的VNode爲
{
asyncFactory: undefined,
asyncMeta: undefined,
children: [VNode, VNode],
componentInstance: undefined,
componentOptions: undefined,
context: Vue實例,
data: {attrs: {id: "app"}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "div",
text: undefined,
child: undefined
}
複製代碼
在組件更新的時候,preVnode和vnode都是存在的,執行
vm.$el = vm.__patch__(prevVnode, vnode);
複製代碼
其實是運行如下函數
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
複製代碼
該函數首先判斷oldVnode和vnode是否相等,相等則當即返回
if (oldVnode === vnode) {
return
}
複製代碼
若是二者均爲靜態節點且key值相等,且vnode是被克隆或者具備isOnce屬性時,vnode的組件實例componentInstance直接賦值
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return
}
複製代碼
接着對二者的屬性值做對比,並更新
var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) { // 以vnode爲準更新oldVnode的不一樣屬性
cbs.update[i](oldVnode, vnode);
}
if (isDef(i = data.hook) && isDef(i = i.update)) {
i(oldVnode, vnode);
}
}
複製代碼
vnode和oldVnode的對比以及相應的DOM操做具體以下:
// vnode不存在text屬性的狀況
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 子節點不相等時,更新
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {
{
checkDuplicateKeys(ch);
}
// 只存在vnode的子節點,若是oldVnode存在text屬性,則將元素的文本內容清空,並新增elm節點
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '');
}
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 若是隻存在oldVnode的子節點,則刪除DOM的子節點
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 只存在oldVnode有text屬性,將元素的文本清空
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// node和oldVnode的text屬性都存在且不一致時,元素節點內容設置爲vnode.text
nodeOps.setTextContent(elm, vnode.text);
}
複製代碼
對於子節點的對比,先分別定義oldVnode和vnode兩數組的先後兩個指針索引
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
複製代碼
以下圖: 接下來是一個while循環,在這過程當中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 會逐漸向中間靠攏
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
複製代碼
當oldStartVnode或者oldEndVnode爲空時,兩中間移動
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
}
複製代碼
接下來這一塊,是將 oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 兩兩比對的過程,共四種:
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
複製代碼
第一種: 前前相等比較 若是相等,則oldStartVnode.elm和newStartVnode.elm均向後移一位,繼續比較。 第二種: 後後相等比較 若是相等,則oldEndVnode.elm和newEndVnode.elm均向前移一位,繼續比較。 第三種: 先後相等比較 將oldStartVnode.elm節點直接移動到oldEndVnode.elm節點後面,而後將oldStartIdx向後移一位,newEndIdx向前移動一位。 第四種: 後前相等比較 將oldEndVnode.elm節點直接移動到oldStartVnode.elm節點後面,而後將oldEndIdx向前移一位,newStartIdx向後移動一位。 若是以上均不知足,則
else {
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
複製代碼
createkeyToOldIdx函數的做用是創建key和index索引對應的map表,若是仍是沒有找到節點,則新建立節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
複製代碼
插入到oldStartVnode.elm節點前面,不然,若是找到了節點,並符合sameVnode,將兩個節點patchVnode,並將該位置的老節點置爲undefined,同時將vnodeToMove.elm移到oldStartVnode.elm的前面,以及newStartIdx日後移一位,示意圖以下: 若是不符合sameVnode,只能建立一個新節點插入到 parentElm 的子節點中,newStartIdx 日後移動一位。 最後若是,oldStartIdx > oldEndIdx,說明老節點比對完了,可是新節點還有多的,須要將新節點插入到真實 DOM 中去,調用 addVnodes 將這些節點插入便可;若是知足 newStartIdx > newEndIdx 條件,說明新節點比對完了,老節點還有多,將這些無用的老節點經過 removeVnodes 批量刪除便可。到這裏這個過程基本結束。
本文詳細介紹了虛擬DOM的整個patch過程,如何到渲染到頁面,以及元素從視圖中刪除,最後是子節點的更新過程,包括了建立新增的子節點、刪除廢棄子節點、更新發生變化的子節點以及位置發生變化的子節點更新等。
剖析 Vue.js 內部運行機制