zoukankan      html  css  js  c++  java
  • Vue的diff算法是如何操作运用的?本文教你

    前言

    本文旨在理一下vue中diff算法的主要逻辑和关键细节。

    从一个简单的demo切入: p标签渲染一个items数组

    <div id="demo">
        <p v-for="item in items" :key="item">{{ item }}</p>
    </div>
    <script src="../vue-source/dist/vue.js"></script>
    <script>
        const app = new Vue({
          el: "#demo",
          data: {
            items: ["a", "b", "c", "d", "e"]
          },
          mounted() {
            setTimeout(() => {
              this.items.splice(2, 0, "f")
            }, 2000)
          }
        })
    </script>
    复制代码

    先把实际顺序说明:

    1. items数据发生变化 Dep.notify
    2. patch(oldVNode, vnode, ...)
    3. patchVnode(oldVnode, vnode, insertedVnodeQueue, ...) ps: diff从这里就开始了 insertedVnodeQueue是patch函数中定义的常量,在后期的diff里面一直维护着,典型的闭包结构。
    4. updateChildren() diff的核心方法
      5.学习要结合实战一起练习的,在此赠送2020最新企业级 Vue3.0/Js/ES6/TS/React/node等实战视频教程,想学的可进裙 519293536 免费获取,小白勿进哦!

    sameVnode

    sameVnode函数贯穿着整个diff过程,其中首要的必要条件就是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)
          )
        )
      )
    }
    复制代码

    key

    众所周知,key在patch中发挥着至关重要的作用,key可以在很多情况下能够有效地减少不必要的重新渲染。 当不设置key时,那么渲染列表数据中的子元素key就是undefined, 显然undefined === undefined。那么sameVNode永远都是相同的(通常情况下),同时造成不必要的渲染(如果是开头的demo中不设置key的话将会多造成3次的不必要渲染)。

    如果设置了key,a.key !== b.key的情况下就马上终止了判断,sameVnode直接返回false,不跟你多bb。
    避免用数组的下标作为key
    因为当数组发生变化时,下标也可能会发生变化,这可能导致一些隐蔽的bug。

    patch

    • 不存在 oldVnode,则进行createElm
    • 存在 oldVnode 和 vnode,但是 sameVnode 返回 false, 则进行createElm
    • 存在 oldVnode 和 vnode,但是 sameVnode 返回 true, 则进行patchVnode

    patchVnode

    可以将Vnode分为3种:

    • 纯文本Vnode
    • 含Children的Vnode
    • 不含Children的Vnode

    所以情况可以分成3*3种

     oldVnode.textoldCh!oldCh
    vnode.text setTextContent setTextContent setTextContent
    ch addVnodes updateChildren addVnodes
    !ch setTextContent removeVnodes setTextContent
    function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
      // 节点相同则直接返回,不作处理
      if (oldVnode === vnode) {
          return
      }
      // ...
      const elm = vnode.elm = oldVnode.elm
      // ...
      const oldCh = oldVnode.children
      const ch = vnode.children
      // ...
      if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
        // 当新老Vnode.length都存在且不相等 进入updateChildren
          if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(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)
      }
      // ...
    }
    复制代码

    updateChildren

    insertedVnodeQueue 是维护的一个数组队列,diff完成后将队列中的数据逐个更新

    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
        // 首首 => 尾尾 => 首尾 => 尾首 => 遍历old用key查找index替换位置
        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]
          }
        }
        // 跳出while循环 也就是前后指针交错了
        // 如果是老节点的指针先交错那就说明是新增了节点 => addVnodes
        // 反之 => removeVnodes
        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(oldCh, oldStartIdx, oldEndIdx)
        }
    复制代码

    测试

    回到例子,为了能走更多的情况,改一下demo。

    <body>
      <div id="demo">
        <p v-for="item in items" :key="item">{{ item }}</p>
      </div>
      <div>
        ["a", "b", "c", "d", "e", "f", "g"] => ["f", "d", "a", "h", "e", "c", "b", "g"]
      </div>
      <script src="../vue/dist/vue.js"></script>
      <script>
        const app = new Vue({
          el: "#demo",
          data: {
            items: ["a", "b", "c", "d", "e", "f", "g"]
          },
          mounted() {
            setTimeout(() => {
              this.items = ["f", "d", "a", "h", "e", "c", "b", "g"]
            }, 2000)
          }
        })
      </script>
    </body>
    复制代码

    在vue.js中的updateChildren打下断点以便观察。

    第一次while

    尾尾gg匹配成功

    newStartVnode都是g, 进入patchVnode。 我原本以为会判断if (oldVnode.text !== vnode.text)然后不作处理,结果竟然又进去了updateChildren。
    不禁让我console.log("oldStartIdx", oldStartIdx, oldCh[oldStartIdx])

    误区

    其中结构为

    我原本以为这个 VNode.text = a; VNode.children = undefined ...
    这个<p>a</p>中的a还是一个VNode... 好在纠正了错误,下面就不管那个纯text的VNode了。

    不会只有我现在才知道吧...

    那么在debugger加个条件以便观察 oldStartVnode.tag === 'p'

    继续

    第二次while

    尾首匹配成功,参考节点为a节点执行nodeOps.insertBefore

    abcdefg => fabcdeg

    oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx];

    第三次while

    首尾四次匹配都没有匹配到,进入该代码块。

    1. 对剩下的oldVnode进行createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)(值见下图)
    2. 如果idxInOld不存在,则表明是new element => createElm
    3. 如果idxInOld存在 => oldCh[idxInOld] = undefined => 参考节点为a节点执行nodeOps.insertBefore
    4. 此时界面上也从 fabcdeg => fdabceg

    第四次while

    首首aa对比成功

    第五次while

    首尾bb对比成功,参考节点为g节点执行nodeOps.insertBefore

    fdabceg => fdacebg

    第六次while

    首尾cc对比成功,参考节点为b节点执行nodeOps.insertBefore

    fdacebg => fdaecbg

    第七次while

    此时的oldStartVnode === undefined => oldStartVnode = oldCh[++oldStartIdx];

    第八次while

    尾尾ee匹配成功

    此时oldEndIdx < oldStartIdx 跳出while,进入下面代码块。

    oldEndIdx < oldStartIdx => 新增了节点 => 参考节点为E节点执行addVnodes()(addVnodes执行最终也是nodeOps.insertBefore),将newCh剩余的节点(H)依次插入E(下面的refElm)节点之前。

    再在debugger中按一次F8就完成了整个diff过程了,现在呈现的就是最终的fdahecbg

    参考链接

    总结

    强烈建议用Chrome浏览器进行调试,配合图简直不要太好理解

    1. 渲染列表的diff关键函数updateChildren
    2. 注意VNode结构 => <p>a<p> => VNode{tag: 'p', children: VNode{tag: undefined, children: undefined, text: 'a'}, text: undefined}
    3. 每一次循环: 首首 => 尾尾 => 首尾 => 尾首 => findIndex => createElm(!idxInOld) | nodeOps.insertBefore(idxInOld)
    4. 跳出循环 => addVnodes(oldStartIdx > oldEndIdx) | removeVnodes(newStartIdx > newEndIdx)

    最后
    注意:学习要结合实战一起练习的,在此赠送2020最新企业级 Vue3.0/Js/ES6/TS/React/node等实战视频教程,想学的可进裙 519293536 免费获取,小白勿进哦!

    本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理

  • 相关阅读:
    22、Flyweight 享元模式
    js随机点名器(简单)
    js随机点名器(简单)
    PHP
    PHP
    Laravel框架实现利用监听器进行sql语句记录功能
    Laravel框架实现利用监听器进行sql语句记录功能
    PhpStorm常用的一些快捷键
    PhpStorm常用的一些快捷键
    HTTP状态码汇总
  • 原文地址:https://www.cnblogs.com/chengxuyuanaa/p/13070834.html
Copyright © 2011-2022 走看看