Vue中采纳了 虚构DOM + Diff算法 缩小了对DOM的操作次数,大大提高了性能,那么咱们明天就来具体的讲一下Vue中这一部分的实现逻辑,心愿能够帮忙还不了解这部分的小伙伴了解这一部分,纯手打,心愿各位小伙伴点个赞反对一下!


首先咱们要明确的是,vnode代表本次批改后新生成的虚构节点,oldVnode代表目前实在DOM构造所对应的虚构节点。所以咱们更新是以vnode为基准,通过oldVnode的构造去操作实在DOM,vnode和oldVnode都不会被扭转,被扭转的只有实在DOM构造。

patch

Vue在首次渲染和数据更新的时候,会去调用 _update 办法,而 _update 办法的外围就是去调用 vm.__patch__ 办法,那么咱们就先从patch办法动手。patch办法中的逻辑比较复杂,这里咱们只看次要流程局部,大抵流程如下图:

function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {    if (isUndef(oldVnode)) {      /*oldVnode未定义的时候,创立一个新的节点*/      isInitialPatch = true      createElm(vnode, insertedVnodeQueue, parentElm, refElm)    } else {      /*标记旧的VNode是否有nodeType*/      const isRealElement = isDef(oldVnode.nodeType)      if (!isRealElement && sameVnode(oldVnode, vnode)) {        /*是同一个节点的时候间接批改现有的节点*/        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)      } else {        if (isRealElement) {          oldVnode = emptyNodeAt(oldVnode)        }        // 插入到视图中旧节点的旁边        const oldElm = oldVnode.elm        const parentElm = nodeOps.parentNode(oldElm)        createElm(          vnode,          insertedVnodeQueue,          oldElm._leaveCb ? null : parentElm,          nodeOps.nextSibling(oldElm)        )        if (isDef(parentElm)) {          /*移除老节点*/          removeVnodes(parentElm, [oldVnode], 0, 0)        } else if (isDef(oldVnode.tag)) {          /*调用destroy钩子*/          invokeDestroyHook(oldVnode)        }      }    }    return vnode.elm}

这里有个 isRealElement 用来标识oldVnode是否有 nodeType,有nodeType就阐明这是个真是的dom节点,要通过emptyNodeAt转化为vNode。

patch中用到了两个比拟重要而办法,一个是createElm,另一个是patchVnode

createElm

createElm 的作用是通过虚构节点创立实在的 DOM 并插入到它的父节点中,次要流程如下:

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {    /*创立一个组件节点*/    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {      return    }    const data = vnode.data    const children = vnode.children    const tag = vnode.tag    if (isDef(tag)) {      vnode.elm = vnode.ns        ? nodeOps.createElementNS(vnode.ns, tag)        : nodeOps.createElement(tag, vnode) // 创立dom节点      setScope(vnode)      if (__WEEX__) {           // ...      } else {        createChildren(vnode, children, insertedVnodeQueue)        if (isDef(data)) {          invokeCreateHooks(vnode, insertedVnodeQueue)        }        insert(parentElm, vnode.elm, refElm)      }    } else if (isTrue(vnode.isComment)) {      vnode.elm = nodeOps.createComment(vnode.text)      insert(parentElm, vnode.elm, refElm)    } else {      vnode.elm = nodeOps.createTextNode(vnode.text)      insert(parentElm, vnode.elm, refElm)    }  }

createComponent 只有在vnode是组件的时候才会返回true,这部分当前再剖析。

createChildren 实际上是遍历子虚构节点,递归调用 createElm,遍历过程中会把 vnode.elm 作为父容器的 DOM 节点传入,因为是递归调用,子元素会优先调用 insert所以整个vnode树节点的插入程序是先子后父

patchVnode

patchVnode的次要流程和次要代码如下:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {    /*两个VNode节点雷同则间接返回*/    if (oldVnode === vnode) {      return    }    /*      如果新旧VNode都是动态的,同时它们的key雷同(代表同一节点),      并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),      那么只须要替换elm以及componentInstance即可。    */    if (isTrue(vnode.isStatic) &&        isTrue(oldVnode.isStatic) &&        vnode.key === oldVnode.key &&        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {      vnode.elm = oldVnode.elm      vnode.componentInstance = oldVnode.componentInstance      return    }    const elm = vnode.elm = oldVnode.elm    const oldCh = oldVnode.children    const ch = vnode.children        /*如果这个VNode节点没有text文本时*/    if (isUndef(vnode.text)) {      if (isDef(oldCh) && isDef(ch)) {        /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)      } else if (isDef(ch)) {        /*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,而后为以后节点退出子节点*/        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)      } else if (isDef(oldCh)) {        /*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/        removeVnodes(elm, oldCh, 0, oldCh.length - 1)      } else if (isDef(oldVnode.text)) {        /*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以间接去除ele的文本*/        nodeOps.setTextContent(elm, '')      }    } else if (oldVnode.text !== vnode.text) {      /*当新老节点text不一样时,间接替换这段文本*/      nodeOps.setTextContent(elm, vnode.text)    }      }

其实也就几种状况:

  1. 新旧vnode都有子节点,对子节点进行diff。
  2. 新vnode有,旧vnode没有,新增。
  3. 旧vnode有,新vnode没有,删除。
  4. 如果有text属性,替换文本。

次要来看其中调用的 updateChildren 办法。

updateChildren

在整个 patchVnode 过程中,最简单的就是 updateChildren 办法了,这个办法里进行了新旧虚构节点子节点的比照。

如何比照新旧子节点列表呢?很简略,循环!循环新子节点列表,其中每一项再去旧子节点列表里循环查找,而后做解决。但通常状况下,并不是所有子节点的地位都会产生扭转。

举个例子,当初有一个列表,咱们对它的操作大多数就是新增一项,删除一项或者扭转其中的一项,大多数节点的地位是不变的,是可预测的,没必要每次都去循环查找,所以Vue中应用了一种更快捷的查找形式,大大提高了性能,简略来说就是头尾比拟

  • 新头:新子节点列表中所有 未解决 的第一个节点
  • 旧头:旧子节点列表中所有 未解决 的第一个节点
  • 新尾:新子节点列表中所有 未解决 的最初一个节点
  • 旧尾:旧子节点列表中所有 未解决 的最初一个节点

Vue中用四个变量 newStartIdxnewEndIdxoldStartIdxoldEndIdx来标识新旧头部与尾部节点,

1. 新头与旧头

如果新头与旧头是同一节点,它们的地位也一样,所以只需更新节点即可,没有挪动操作。

2. 新尾与旧尾

如果新尾与旧尾是同一节点,它们的地位也一样,所以只需更新节点即可,没有挪动操作。

3. 新尾与旧头

如果新尾与旧头是同一节点,因为它们地位不一样,所以除了更新,还要进行挪动操作。

首先咱们要明确的是更新是以vnode为基准,oldVnode代表的就是实在DOM构造,所以咱们要更新实在DOM其实就是去更新oldVnode。

这里咱们要留神,肯定是挪动到所有 未解决节点 前面,因为新尾是新子节点列表里未解决 的最初一个。

4. 新头与旧尾

如果新头与旧尾是同一节点,因为它们地位不一样,所以除了更新,还要进行挪动操作。
挪动逻辑和下面的 新尾与旧头 大致相同,把旧尾挪动到所有 未解决节点之前


如果通过这四次比照还是没有在 旧子节点列表 中找到雷同的节点,那么先用oldVnode生成一个key为oldVnode的key,value为对应下标的哈希表 {key0: 0, key1: 1},而后用 新头(newStartVnode) 的key在哈希表里查找,如果找到对应的,判断他们是不是 sameVnode

  1. 如果是,那么就示意在 旧子节点列表 中找到了雷同的节点,进行更新节点操作,最初还要将这个老节点赋值 undefined,防止后续有雷同的key反复比照。
  2. 如果不是,那么就有可能是标签不一样了,或者input的type扭转了,这时间接创立一个新的节点。

如果在哈希表里没找到对应的,就简略了,间接创立一个新的节点。最初遍历完结,会有两种状况:

  1. oldStartIdx > oldEndIdx,阐明老节点遍历完结,那么残余新节点就是要新增的,那么一个一个创立进去退出到实在Dom中。
  2. newStartIdx > newEndIdx,阐明新节点遍历完结,那么残余的老节点就是要删除的,删除即可。

逻辑理分明了,是不是感觉也很简略呢!

结尾

我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~