上回咱們瞭解了
vnode
從建立到生成的流程,這回咱們來探索Vue
是如何將vnode
轉化成真實的dom
節點/元素javascript
Vue.prototype._update
上次咱們提到的 _render
函數其實做爲 _update
函數的參數傳入,換句話說,_render
函數結束後 _update
將會執行👇前端
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
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.
};
複製代碼
簡單梳理下這段代碼的邏輯:vue
setActiveInstance(vm)
設置當前的 vm
爲活躍的實例preVnode
是否存在,是則調用 vm.$el = vm.__patch__(prevVnode, vnode);
,不然調用 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
(其實也就是第一次渲染跟二次更新的區別)restoreActiveInstance()
重置活躍的實例HOC
作了特殊判斷(由於沒用過 HOC
,因此這裏直接略過)從上面整理下來的邏輯中,咱們能獲得訊息僅僅只有 setActiveInstance
函數返回一個閉包函數(固然這並非很重要),若是須要更深刻的瞭解,還須要瞭解 __patch__
函數是怎麼實現的java
其餘相關代碼:node
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
...
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
複製代碼
__patch__
說出來你可能不信,__patch__
函數的實現其實很簡單👇api
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
...
Vue.prototype.__patch__ = inBrowser ? patch : noop;
複製代碼
很明顯,createPatchFunction
也是返回了一個閉包函數微信
patch
雖然 __patch__
外表看起來很簡單,可是其實內部實現的邏輯仍是挺複雜的,代碼量也很是多👇閉包
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
);
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(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)
);
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
var ancestor = vnode.parent;
var patchable = isPatchable(vnode);
while (ancestor) {
for (var i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
var insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
insert.fns[i$2]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
複製代碼
這麼多的代碼,一會兒確定是消化不完的,因此咱們能夠嘗試性的帶着如下這幾個問題來看👇app
patch
操做與後續的 patch
操做有何區別?dom
節點之間產生變動,或者說是「新節點」替換「老節點」時,規則是怎麼樣的?patch
函數的特殊邏輯針對初次渲染,patch
函數是作了特殊邏輯的。顯然咱們只要把初次執行的 patch
的邏輯走一遍就清楚了👇dom
結合上面的源碼,概括下這裏的思路:
createElm(vnode, insertVnodeQueue)
來 直接建立「新節點」dom
節點,則分紅如下幾步:
SSR_ATTR
屬性(若存在)hydrating
)
hydrate(oldvnode, vnode, insertVnodeQueue)
並判斷是否執行成功
invokeInsertHook(vnode, insertVnodeQueue, true)
emptyNodeAt(oldVnode)
,給「老節點」(其實是 dom
節點)生成它的 "vnode
"看完源碼的同窗不難不發現,上面梳理的邏輯裏少了這段代碼:
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
}
複製代碼
也就是對「非 dom
元素的相同節點」作一次 patchVnode
的操做。關於這段代碼能夠分紅幾點來分析:
patchVnode
作了什麼?根據語義咱們應該看這部分代碼👇
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
複製代碼
sameVnode
的邏輯就是:按照 vnode
的屬性來判斷兩個 「vnode
」節點是不是同一個節點
patchVnode
因爲執行 patchVnode
的前提就是新老節點是「相同」的節點,咱們有理由相信,它是用來處理同個節點的變化。
function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) {
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
var elm = vnode.elm = oldVnode.elm;
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return
}
var i;
var data = vnode.data;
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
var oldCh = oldVnode.children;
var 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); }
}
}
複製代碼
咱們看看這段代碼都作了哪些事情:
vnode
(若是存在 elem
屬性)prepatch
(若是存在 data
屬性)update
(若是存在 data
屬性)oldVnode
和 vnode
兩個節點postpatch
(若是存在 data
屬性)固然,這裏最直觀的就是比較 oldVnode
和 vnode
兩個節點的邏輯👇
其餘的邏輯能夠留到下一篇文章再分析~
掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。