目錄html
1.patch函數的脈絡vue
2.類vnode的設計node
3.createPatch函數中的輔助函數和patch函數react
4.源碼運行展現(DEMO)web
首先梳理一下patch函數的脈絡。算法
第一,patch核心函數createPatchFunction,api
而後,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框架用於分別執行某個渲染任務的功能函數。weex
根據詳細的截圖,能夠看到每一個模塊完成某個功能,屬性和類、監聽器、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類定義了不少屬性。
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函數包括有關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函數相關的。