zoukankan      html  css  js  c++  java
  • vue2源码分析:patch函数

    目录

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

  • 相关阅读:
    AWS EC2服务器的HTTPS负载均衡器配置过程
    大数据技术Hadoop笔试题
    网上找的hadoop面试题目及答案
    360全景图three.js
    360全景图three.js与Photo-Sphere-Viewer-master 3D全景浏览开发
    @font-face 字体
    scss语法
    6.事件
    5.回调函数
    4.querystring属性
  • 原文地址:https://www.cnblogs.com/chenmeng2062/p/12552302.html
Copyright © 2011-2022 走看看