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 免费获取,小白勿进哦!

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

  • 相关阅读:
    heat模板
    Leetcode812.Largest Triangle Area最大三角形面积
    Leetcode812.Largest Triangle Area最大三角形面积
    Leetcode811.Subdomain Visit Count子域名访问计数
    Leetcode811.Subdomain Visit Count子域名访问计数
    Leetcode806.Number of Lines To Write String写字符串需要的行数
    Leetcode806.Number of Lines To Write String写字符串需要的行数
    Leetcode819.Most Common Word最常见的单词
    Leetcode819.Most Common Word最常见的单词
    Leetcode783.Minimum Distance Between BST Nodes二叉搜索树结点最小距离
  • 原文地址:https://www.cnblogs.com/chengxuyuanaa/p/13070834.html
Copyright © 2011-2022 走看看