目錄
1.patch函數的脈絡
2.類vnode的設計
3.createPatch函數中的輔助函數和patch函數
4.源碼運行展示(DEMO)
一.patch函數的脈絡
首先梳理一下patch函數的脈絡。
第一,patch核心函數createPatchFunction,
然后,runtime/index.js中將patch方法掛載到vue的原型屬性__patch__上。
Vue.prototype.__patch__ = inBrowser ? patch : noop
最后patch的使用是當我們調用vue實例的$el時,即調用patch函數。
if (!prevVnode) { // initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates
vm.$el = vm.__patch__(prevVnode, vnode) }
其中,createPatchFunction函數結構
export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend; ,,,hooks和modules的 for循環 其中const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] 一些輔助函數 emptyNodeAt,createRmCb,removeNode,isUnknownElement,createElm,createComponent ,
initComponent,reactivateComponent, insert, createChildren ,isPatchable ,setScope ,
addVnodes ,invokeDestroyHook , removeVnodes , removeAndInvokeRemoveHook,updateChildren,
checkDuplicateKeys, findIdxInOld , patchVnode , invokeInsertHook ,hydrate, assertNodeMatch 核心函數return patch }
第一,要了解createPatchFunction的參數backend。backend的nodeOps是節點的功能函數,包括createElement創建元素、removeChild刪除子元素,tagName獲取到標簽名等,backend的modules是vue框架用於分別執行某個渲染任務的功能函數。

根據詳細的截圖,可以看到每個模塊完成某個功能,屬性和類、監聽器、DOM屬性、樣式的創建和更新、指令更新以及其他操作

我們知道vue虛擬DOM的比較依賴於diff算法,diff算法到底有什么魔法能快速比較出文本的差異?我們可以手動的寫一個簡易的函數實現diff算法。具體可參照https://www.cnblogs.com/MRRAOBX/articles/10043258.html。
首先,我們先假設一個需求。
<div class = "box">
<ul>
<li> hello,everyone!</li>
</ul>
</div>
var list = document.querySelector( '.list' )
var li = document.createElement( 'LI' )
li.innerHTML = ' 疫情還沒有結束 '
list.appendChild( li )
我們用一個vdom對象模擬上述html結構,並通過render函數渲染出來。然后 數據更改了,data.name = ‘疫情終於結束了’
var vdom = { tag: 'div', attr: { className: 'box' }, content: [ { tag: 'ul', content: [ { tag: 'li', content: data.name } ] } ] }
那么我們通過diff算法比對兩次vdom,生成patch對象,最終實現了打補丁。
二.類vnode的設計
VNode類定義了很多屬性。
export default class VNode { tag: string | void; data: VNodeData | void; // VNode類定義了屬性tag
constructor (){} ....... }
同時提供了提供了一些功能,createEmptyVNode創建空的VNode,createTextVNode創建文本類型的VNode,cloneVNode克隆VNode。
為了方便我們更好的理解這個屬性,我們可以運行源碼,打印一下這個Vnode。我們是不是可以看到最重要的屬性就是tag(標簽名)、data(標簽的屬性-值)、children(所有后代元素)、context(上下文對象)。

附我的html結構
<div id="app">
<div></div>
。。。。。。
</div>
三.createPatch函數中的輔助函數和patch函數
createPatch函數包括有關VNode增刪改查的功能函數
//返回的e
function emptyNodeAt (elm) { return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) } //使用它的地方只有一個
oldVnode = emptyNodeAt(oldVnode);
emptyNodeAt包裝oldVnode前后有什么區別呢?依然是運行源碼,我們發現傳入的參數是dom元素,包裝后變成了VNode,即vue形式的節點實例。

createRmCb功能是創建remove函數
remove$$1函數作為一個對象,第一個參數是vnode所屬的dom元素,第二個參數是監聽器個數。內部實現remove函數擁有listeners屬性,等到這個屬性的值每一次減少直到0時將直接移除節點。這個原理很簡單,要移除某個節點,先要把監聽器一個一個的全部移除掉。
rm = createRmCb(vnode.elm, listeners); //只有一個地方使用了createRmCb
'function createRmCb (childElm, listeners) { function remove$$1 () { if (--remove$$1.listeners === 0) { removeNode(childElm); } } remove$$1.listeners = listeners; return remove$$1 }
removeNode移除節點,先找到父節點,然后通過removeChild移除掉這個節點。那么為什么要這樣操作呢?因為這里的removeChild是原生方法中移除的唯一做法。
function removeNode (el) { const parent = nodeOps.parentNode(el) // element may have already been removed due to v-html / v-text if (isDef(parent)) { nodeOps.removeChild(parent, el) } }
function removeChild (node, child) { node.removeChild(child); }isUnknownElement略。
create***函數
createElm第一個參數是vue node實例,在vnode.js文件中我們已經知道了vnode類的具體情況,第二個參數是數組,表示插入的vnode實例的隊列,第三個參數是parentElm父元素,畢竟原生的
添加元素唯一的方法是先找到父元素,然后appendChild添加元素。第4個參數是refElm,如果子元素包含ref屬性的節點,那么這個參數就有值。第5個參數是nested,值是true或者false.第5個
參數是ownerArray,它是當前節點和兄弟節點組成的數組。第6個是index索引。
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode) }
首先我們對某一種類型的vnode進行了調整。一般情況下vnode的elm都有定義,不過當我用vnode.elm打印時返回undefined(具體原因還不知道,明明打印出來的vnode的elm屬性的呀)。另外,ownerArray有哪些元素不會定義呢,答案是vue項目掛載app的根元素。這樣一來,普通的vnode都不會進入這個if語句。
vnode.isRootInsert = !nested // for transition enter check //根據注釋,它跟vue畫面的漸進效果有關
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } //如果是創建組件,那么直接返回
具體看后面createComponent的功能咯。
const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== 'production') { if (data && data.pre) { creatingElmInVPre++ } if (isUnknownElement(vnode, creatingElmInVPre)) { warn( 'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.', vnode.context ) } }
這一段就是把需要的數據從vnode中取出來,我們上面已經打印過vnode了,復習一下,data 是有關元素key-value的數據信息,chidren是后代元素,tag是標簽名。並有針對開發環境的調試信息。
vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) //namespce命名空間
接下來,weex直接略過。、
else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) }
那么我們看到創建元素調用的核心函數是createChildren和insert。
function createChildren (vnode, children, insertedVnodeQueue) { //若
if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(children) } // for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } //如果是原生類型
else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }
createChildren
function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (nodeOps.parentNode(ref) === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } } function appendChild (node, child) { node.appendChild(child); } function insertBefore (parentNode, newNode, referenceNode) { parentNode.insertBefore(newNode, referenceNode); }
insert
function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { //若ref節點的父元素等於該元素的父元素
if (nodeOps.parentNode(ref) === parent) { //那么通過insertBefore方法將元素ref插入到elm之前
nodeOps.insertBefore(parent, elm, ref) } } else { //添加元素elm
nodeOps.appendChild(parent, elm) } } } //調用insert的例子
vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm)
到底vue是如何創建元素的?我們用簡單的html結構看一下createElm到底是如何運行的(我通過源碼打斷點的方式來看到底發生了什么)
new Vue({ el:"#app",} ); //html結構
<div id="app">
<span>123</span>
</div>

vue項目初始化時首先創建div#app的節點。vnode是div#app的vnode,insertedVnodeQueue為空數組,parentElm是body元素,refElm如圖,refElm到底是什么?它是一個文本節點。

wholeText: "↵" assignedSlot: null data: "↵" length: 1 previousElementSibling: div#app nextElementSibling: script nodeType: 3 nodeName: "#text" baseURI: "http://localhost:63342/vuesrc/1.vue.set%E4%BD%BF%E7%94%A8.html?_ijt=clboq4te5mp0i755tqhvsc3q75" isConnected: true ownerDocument: document parentNode: body parentElement: body childNodes: NodeList [] firstChild: null lastChild: null previousSibling: div#app nextSibling: script nodeValue: "↵" textContent: "↵" __proto__: Text
第二個創建的元素是span。span的refElm是null,nested為true。

第三個創建的是123所代表的文本節點。

我們看到當vue項目要加載某些節點時都會調用它。
createComponent的使用在createElm這一行有這個判斷。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return }
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i = vnode.data; if (isDef(i)) { var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } } }
首先是div#app元素。
在createComponent中判斷vnode.data。div#app判斷isDef(i)為true。
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;

isReactivated和判斷hook和init的if都會返回false。第二個if由於componentInstance: undefined也會false。
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef(i = i.hook) && isDef(i = i.init)) {
第二是span以及文本節點,他們由於data未定義,所以並不會進入外層if語句。
isPatchable
function isPatchable (vnode) { while (vnode.componentInstance) { vnode = vnode.componentInstance._vnode } return isDef(vnode.tag) }
invokeCreateHooks
div#app的創建時會調用invokeCreateHooks

cbs的內容是
create: (8) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ] activate: [ƒ] update: (7) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ] remove: [ƒ] destroy: (2) [ƒ, ƒ] __proto__: Object
。。。。
create: Array(8)
0: ƒ updateAttrs(oldVnode, vnode)
1: ƒ updateClass(oldVnode, vnode)
2: ƒ updateDOMListeners(oldVnode, vnode)
3: ƒ updateDOMProps(oldVnode, vnode)
4: ƒ updateStyle(oldVnode, vnode)
5: ƒ _enter(_, vnode)
6: ƒ create(_, vnode)
7: ƒ updateDirectives(oldVnode, vnode)
length:
__proto__: Array(0)
那么函數調用后發生了什么呢?cbs.create是一個函數作為成員的數組,遍歷每個成員調用,我們以其中一個成員函數來看看發生了什么,updateAttrs(emptyNode,vnode)。
function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } }
我們找到updateAttrs方法。
function updateAttrs (oldVnode, vnode) { var opts = vnode.componentOptions; if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) { return } if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { return } var key, cur, old; var elm = vnode.elm; var oldAttrs = oldVnode.data.attrs || {}; var attrs = vnode.data.attrs || {}; // clone observed objects, as the user probably wants to mutate it if (isDef(attrs.__ob__)) { attrs = vnode.data.attrs = extend({}, attrs); } //核心代碼,setAttr設置新節點的屬性 for (key in attrs) { cur = attrs[key]; old = oldAttrs[key]; if (old !== cur) { setAttr(elm, key, cur); } } // #4391: in IE9, setting type can reset value for input[type=radio] // #6666: IE/Edge forces progress value down to 1 before setting a max /* istanbul ignore if */ if ((isIE || isEdge) && attrs.value !== oldAttrs.value) { setAttr(elm, 'value', attrs.value); } //核心代碼,刪除糾結點的屬性 for (key in oldAttrs) { if (isUndef(attrs[key])) { if (isXlink(key)) { elm.removeAttributeNS(xlinkNS, getXlinkProp(key)); } else if (!isEnumeratedAttr(key)) { elm.removeAttribute(key); } } } }
function setAttr (el, key, value) { if (el.tagName.indexOf('-') > -1) { baseSetAttr(el, key, value); } else if (isBooleanAttr(key)) { // set attribute for blank value // e.g. <option disabled>Select one</option> if (isFalsyAttrValue(value)) { el.removeAttribute(key); } else { // technically allowfullscreen is a boolean attribute for <iframe>, // but Flash expects a value of "true" when used on <embed> tag value = key === 'allowfullscreen' && el.tagName === 'EMBED' ? 'true' : key; el.setAttribute(key, value); } } else if (isEnumeratedAttr(key)) { el.setAttribute(key, convertEnumeratedValue(key, value)); } else if (isXlink(key)) { if (isFalsyAttrValue(value)) { el.removeAttributeNS(xlinkNS, getXlinkProp(key)); } else { el.setAttributeNS(xlinkNS, key, value); } } else { baseSetAttr(el, key, value); } }
function baseSetAttr (el, key, value) { if (isFalsyAttrValue(value)) { el.removeAttribute(key); } else { // #7138: IE10 & 11 fires input event when setting placeholder on // <textarea>... block the first input event and remove the blocker // immediately. /* istanbul ignore if */ if ( isIE && !isIE9 && el.tagName === 'TEXTAREA' && key === 'placeholder' && value !== '' && !el.__ieph ) { var blocker = function (e) { e.stopImmediatePropagation(); el.removeEventListener('input', blocker); }; el.addEventListener('input', blocker); // $flow-disable-line el.__ieph = true; /* IE placeholder patched */ } el.setAttribute(key, value); } }
然后就是data.hook有沒有定義。要是定義了,那就調用create或者insert方法。
setScope
function setScope (vnode) { let i if (isDef(i = vnode.fnScopeId)) { nodeOps.setStyleScope(vnode.elm, i) } else { let ancestor = vnode while (ancestor) { if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) { nodeOps.setStyleScope(vnode.elm, i) } ancestor = ancestor.parent } } // for slot content they should also get the scopeId from the host instance. if (isDef(i = activeInstance) && i !== vnode.context && i !== vnode.fnContext && isDef(i = i.$options._scopeId) ) { nodeOps.setStyleScope(vnode.elm, i) } }
addVnodes
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) { for (; startIdx <= endIdx; ++startIdx) { createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx) } }
invokeDestroyHook
function invokeDestroyHook (vnode) { let i, j const data = vnode.data if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode) for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) } if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]) } } }
destroy調用實際上是調用的function destory以及unbindDirectives 。那么功能是銷毀咯。
destroy: Array(2) 0: ƒ destroy(vnode) 1: ƒ unbindDirectives(vnode)
destroy: function destroy (vnode) { var componentInstance = vnode.componentInstance; if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy(); } else { deactivateChildComponent(componentInstance, true /* direct */); } } }
destroy: function unbindDirectives (vnode) { updateDirectives(vnode, emptyNode); }
removeVnodes刪除vnode做了哪些事情,刪除hook,刪除元素。
function removeVnodes (parentElm, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx] if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch) invokeDestroyHook(ch) } else { // Text node removeNode(ch.elm) } } } }
removeNode的原生方法其實就是removeChild。
function removeNode (el) { var parent = nodeOps.parentNode(el); // element may have already been removed due to v-html / v-text if (isDef(parent)) { nodeOps.removeChild(parent, el); } }
rm一開始為undefined,通過 rm = createRmCb(vnode.elm, listeners) 創建了remove函數。

核心代碼是 cbs.remove[i](vnode, rm) 其實就回到了remove函數這里。
function remove () { if (--remove.listeners === 0) { removeNode(childElm) } }
updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } 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] } 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] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
checkDkeys
function checkDuplicateKeys (children) { const seenKeys = {} for (let i = 0; i < children.length; i++) { const vnode = children[i] const key = vnode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, vnode.context ) } else { seenKeys[key] = true } } } }
findIdsInOld
function findIdxInOld (node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i } }
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) } const 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 } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } 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) } }
invokeInsertHook
function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } }
assertNodeMatch
function assertNodeMatch (node, vnode, inVPre) { if (isDef(vnode.tag)) { return vnode.tag.indexOf('vue-component') === 0 || ( !isUnknownElement(vnode, inVPre) && vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()) ) } else { return node.nodeType === (vnode.isComment ? 8 : 3) } }
核心函數patch
首先,通過示例給patch函數打斷點,我們看到第一個參數是div#app dom元素,第二個參數是包含div#app信息的vnode。第一部分的代碼並沒有進入if語句
if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = []
第二部分由於oldNode已經定義所以分支語句進入else分支。else分支首先處理如果oldVnode是元素的一些操作。然后createElm創建元素。第三,如果存在父元素,對祖先元素遍歷,那么對祖先元素注冊鈎子函數,否則世界registerRef。 ancestor = ancestor.parent 是while循環的條件。接下來刪除舊的節點。第四,invokeInsertHook。最后返回vnode的dom元素。
if (isUndef(oldVnode)){}else{ //dom元素的nodeType為1,所以isDef返回true const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } //!isRealElement為false,進入else分支 else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. //根據var SSR_ATTR = 'data-server-rendered',我們看到如果是服務端渲染 //那么元素移除掉SSR-ATTR屬性,並且hydrating設置為true if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } //如果我們要設置hydrating,那么就插入鈎子函數 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 //emptyNodeAt將oldVnode包裝一下 oldVnode = emptyNodeAt(oldVnode) } // replacing existing element const oldElm = oldVnode.elm const 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)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } }
四.源碼運行展示
虛擬DOM並不能改變DOM操作本身很慢的情況,它通過對象模擬DOM節點,它的優化點有兩個部分
-
初始化文檔結構時,先js構建出一個真實的DOM結構,然后再插入文檔。
-
更新試圖時,將新舊節點樹比較計算出最小變更然后再映射到真實的DOM中。這在大量、頻繁的更新數據時有很大的優勢。
這也是patch函數的功能。
DEMO1.初次渲染
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue初次渲染</title> <script src="js/vue.js"></script> </head> <body> <div id="app"> <span>{{obj}}</span> </div> <script> new Vue({ el:"#app", data:{ obj:"012" }, created:function(){ this.obj="567"; }, methods:{ addName(){ this.obj2=this.obj2+"456" } } }) </script> </body> </html>
我們把vue.js打斷點。
首先在function lifecycleMixin 中調用 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
其中 Vue.prototype.__patch__ = inBrowser ? patch : noop; 目前我們只考慮瀏覽器有DOM的情況。vm.$el就是div#app節點,vnode是div#app包裝成的虛擬節點。
然后執行patch函數,
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);
}
//這些邏輯都不會進入
由於oldNode參數是div#app,它是真正的元素節點,emptyNodeAt之后什么變化呢?它將dom節點變成虛擬節點。
if (isRealElement) { //SSR渲染的邏輯略過。 oldVnode = emptyNodeAt(oldVnode); }
然后createElm,這個函數的核心代碼是 insert(parentElm, vnode.elm, refElm) 那么我們的節點vnode.elm就插入了DOM中。
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) );
function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (nodeOps.parentNode(ref) === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } } //通過insertBefore或者appendChild添加元素
由於vue項目掛載的節點的parent為undefined,所以 if (isDef(vnode.parent)) { 為false不進入。
然后掛載的節點的父元素是body,存在即true,那么刪除舊的節點。
if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) }
為什么要刪除舊的節點?
因為createElm加入的節點是與虛擬DOM關聯的節點,瀏覽器本身還有渲染節點的。從圖示打斷點,當運行到removeVnodes時,這個時候還未刪除就出現了兩行元素。當我們運行完所有代碼后才能顯示正常結果。

正常結果圖示

最后 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) 將隊列中的鈎子函數插入到隊列的hook中。
function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue; } else { for (var i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]); } } }
DEMO2.
需求是我們要展示一個個產品列表,而且我們這個DEMO使用模塊化開發的方式。我們首先來看一看初次渲染的情況。
先上代碼。目錄結構是vue官方腳手架。

核心代碼是
//App.vue <template> <div> <img src="./assets/logo.png"> <ul> <li v-for="item in items"> {{ item.message }}---{{item.id}} </li> </ul> <!--<router-view/>--> </div> </template> <script> import Vue from "vue" export default { name: 'App', data(){ return{ items:[ {id:1101,message:"VERSACE范思哲"}, {id:1102,message:"GUCCI古馳男士經典蜜蜂刺綉"}, {id:1103,message:"BURBERRY巴寶莉男士休閑長袖襯衫"}, {id:1104,message:"BALLY巴利奢侈品男包"}, {id:1105,message:"FERRAGAMO菲拉格慕男款休閑皮鞋"} ] } }, methods:{ } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } li{ list-style: none; } </style>
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>vue-demo</title> </head> <body> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
我們依然在Sources面板找到模塊中vue源碼打斷點。

oldNode的結構是

vnode的結構是


我們看到vnode的tag名稱是vue-component-4-App。
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 { //核心代碼 oldVnode = emptyNodeAt(oldVnode); }
emptyNodeAt將原有的節點,同時也是DOM節點包裝成虛擬節點。
// replacing existing element var oldElm = oldVnode.elm; var parentElm = nodeOps.parentNode(oldElm); //parentElm是undefined //創建新節點 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) );
進入createElm函數。vnode是tag名為vue-component-4-App的虛擬節點。parentElm是body元素。

createElm函數中由於ownerArray等於undefined,所以打頭的if語句為false。接下來到createComponent函數。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return }


if (isDef(i)) { var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
//根據vnode.data的結構,通過賦值,i調用的是init鈎子函數。 if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } }
那么初始化init鈎子函數調用, child.$mount(hydrating ? vnode.elm : undefined, hydrating); 由於hydrating為false,進而進入mount函數。


mountComponent執行了 callHook(vm, 'beforeMount'); 然后運行了update。接下來掛載了watcher。
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 */);
然后又回到了createElm函數。

這里的vnode指的是template中的包裹元素。它的父元素是剛才的tag為vue-component-4-App的元素。
//vnode結構 child: (...) tag: "div" data: undefined children: (3) [VNode, VNode, VNode] text: undefined elm: undefined ns: undefined context: VueComponent {_uid: 1, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …} fnContext: undefined fnOptions: undefined fnScopeId: undefined key: undefined componentOptions: undefined componentInstance: undefined parent: VNode {tag: "vue-component-4-App", data: {…}, children: undefined, text: undefined, elm: undefined, …} raw: false isStatic: false isRootInsert: true isComment: false isCloned: false isOnce: false asyncFactory: undefined asyncMeta: undefined isAsyncPlaceholder: false __proto__: Object
<template>
<div>
<img src="./assets/logo.png">
<ul>
<li v-for="item in items">
{{ item.message }}---{{item.id}}
</li>
</ul>
<!--<router-view/>-->
</div>
</template>

這時 createChildren(vnode, children, insertedVnodeQueue); 創建各個子元素。通過遍歷,最終會將所有子元素通過insert添加到tag為vue-component-4-App的元素上。

最終patch函數返回 return vnode.elm 節點。




從這個分析可以看到初次渲染,會把所有節點最終加入template中的div元素,等到了tag為vue-component-4-App的元素,由於isDef(parentElm)的parentElm為body元素,所以為true。這個時候也可以看到DOM元素有兩份,那么就要刪除舊的元素 removeVnodes(parentElm, [oldVnode], 0, 0); 。最終運行完畢,呈現正確的DOM結構。
當還沒有運行removeVnodes時DOM結構如截圖2。
圖1

圖2

運行完removeVnodes后原有的div#app就被刪除了。

初次渲染我們也可以看到,總是把所有子元素構成的render樹渲染好了再一次性添加到文檔中。
DEMO3
需求是ul中動態刪除某個li標簽。我們知道要使用唯一ID的key,才能更高效的渲染。我們可以來看一下patch函數中到底發生了什么?
其他內容同DEMO2,也是按模塊化開發來的。
//App.vue
<template>
<div>
<img src="./assets/logo.png">
<ul>
<li v-for="item in items">
{{ item.message }}---{{item.id}}
</li>
</ul>
<button v-on:click="addItem()">添加item</button>
<!--<router-view/>-->
</div>
</template>
<script>
import Vue from "vue"
export default {
name: 'App',
data(){
return{
items:[
{id:1101,message:"VERSACE范思哲"},
{id:1102,message:"GUCCI古馳男士經典蜜蜂刺綉"},
{id:1103,message:"BURBERRY巴寶莉男士休閑長袖襯衫"},
{id:1104,message:"BALLY巴利奢侈品男包"},
{id:1105,message:"FERRAGAMO菲拉格慕男款休閑皮鞋"}
]
}
},
methods:{
addItem(){
this.items.splice(2,1,{id:1106,message:"GUCCI古奇新款小蜜蜂刺綉低幫休閑板鞋男"})
} } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } li{ list-style: none; } </style>
點擊按鈕 this.items.splice(2,1) 就會添加一個item。
我們這次在function renderList打斷點。
//App.vue <template> <div> <img src="./assets/logo.png"> <ul> <li v-for="item in items" > {{ item.message }}---{{item.id}} </li> </ul> <button v-on:click="addItem()">添加item</button> <!--<router-view/>--> </div> </template> <script> import Vue from "vue" export default { name: 'App', data(){ return{ items:[ {id:1101,message:"VERSACE范思哲"}, {id:1102,message:"GUCCI古馳男士經典蜜蜂刺綉"}, {id:1103,message:"BURBERRY巴寶莉男士休閑長袖襯衫"}, {id:1104,message:"BALLY巴利奢侈品男包"}, {id:1105,message:"FERRAGAMO菲拉格慕男款休閑皮鞋"} ] } }, methods:{ addItem(){ this.items.push({id:1106,message:"GUCCI古奇新款小蜜蜂刺綉低幫休閑板鞋男"}); } } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } li{ list-style: none; } </style>
首先看初次渲染時的參數情況。val為包含5個子元素的類數組。進入第一個if分支,render返回li標簽的虛擬節點,節點含有並且含有key屬性,並添加到ret數組。


if (Array.isArray(val) || typeof val === 'string') { ret = new Array(val.length); for (i = 0, l = val.length; i < l; i++) { ret[i] = render(val[i], i); } }
如果我們push新的值,ret為6個元素了。那么接下來就會打斷點運行到patchVnode,其中sameVnode通過key來比較是否是同一個節點。
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) ) ) )
//如果舊的虛擬節點和新的節點是相同的,那么不用作渲染。 if (oldVnode === vnode) { return }
更詳細的參考一些v-for指令的源碼,這里只涉及patch函數相關的。
