vue2源码分析:patch函数

  • 2020 年 3 月 30 日
  • 笔记

目录

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);      }    }

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函数相关的。