vue2源碼分析:patch函數

目錄html

1.patch函數的脈絡vue

2.類vnode的設計node

3.createPatch函數中的輔助函數和patch函數react

4.源碼運行展現(DEMO)web

一.patch函數的脈絡

首先梳理一下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的設計

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);
    }
  }
setAttr
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節點,它的優化點有兩個部分

  1. 初始化文檔結構時,先js構建出一個真實的DOM結構,而後再插入文檔。

  2. 更新試圖時,將新舊節點樹比較計算出最小變動而後再映射到真實的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函數相關的。

相關文章
相關標籤/搜索