• 技术文章 >web前端 >Vue.js

    一文搞懂vue2 diff算法(附图)

    青灯夜游青灯夜游2022-11-10 20:55:43转载265

    大前端零基础入门到就业:进入学习

    vue2的diff过程

    以下源码来自vue/patch.ts,会有一些提取,相关函数会附上链接。【相关推荐:vuejs视频教程web前端开发

    patch函数

      return function patch(oldVnode, vnode, hydrating, removeOnly) {
          if (isUndef(vnode)) {  //新的节点不存在
              if (isDef(oldVnode)) //旧的节点存在
              invokeDestroyHook(oldVnode)   //销毁旧节点
              return
           }
             .........
          //isRealElement就是为处理初始化定义的,组件初始化的时候,没有oldVnode,那么Vue会传入一个真实dom
          if (!isRealElement && sameVnode(oldVnode, vnode)) { -----判断是否值得去比较
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) ---打补丁,后面会详细讲
          } else {
            ......
             if (isRealElement) 
             ......
              oldVnode = emptyNodeAt(oldVnode) //转化为Vnode,并赋值给oldNode
            }
            // replacing existing element
            const oldElm = oldVnode.elm      ----找到oldVnode对应的真实节点
            const parentElm = nodeOps.parentNode(oldElm)  ------找到它的父节点
            createElm(.....) --------创建新节点
            ....递归地去更新节点
        return vnode.elm
      }

    image.png

    sameNode函数

    function sameVnode(a, b) {
      return (
            a.key === b.key &&  ----------------------key值相等, 这就是为什么我们推荐要加上key,可以让判断更准确
        a.asyncFactory === b.asyncFactory && 
        ((a.tag === b.tag && ---------------------标签相等
          a.isComment === b.isComment && ---------是否为注释节点
          isDef(a.data) === isDef(b.data) &&  ----比较data是否都不为空
          sameInputType(a, b)) ||  ---------------当标签为input的时候,需要比较type属性
          (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
      )
    }

    patchVNode

      function patchVnode(...
      ) {
        if (oldVnode === vnode) {  //两个节点一致,啥也不用管,直接返回
          return
        }
        ....
        if (
        //新旧节点都是静态节点,且key值相等,则明整个组件没有任何变化,还在之前的实例,赋值一下后直接返回
          isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key &&
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        }
        const oldCh = oldVnode.children  //获取旧节点孩子
        const ch = vnode.children //获取新节点孩子
        if (isUndef(vnode.text)) { //新节点没有文本
          if (isDef(oldCh) && isDef(ch)) {  //旧节点孩子和新节点孩子都不为空
            if (oldCh !== ch) //旧节点孩子不等于新节点孩子
              updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) //重点----比较双方的孩子进行diff算法 
          } else if (isDef(ch)) {  //新节点孩子不为空,旧节点孩子为空
             ....
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) //新增节点
           } else if (isDef(oldCh)) {  //新节点孩子为空,旧节点孩子不为空
            removeVnodes(oldCh, 0, oldCh.length - 1)  //移除旧节点孩子节点
          } else if (isDef(oldVnode.text)) {  //旧节点文本为不为空
            nodeOps.setTextContent(elm, '')  //将节点文本清空
          }
        } else if (oldVnode.text !== vnode.text) { //新节点有文本,但是和旧节点文本不相等
          nodeOps.setTextContent(elm, vnode.text) //设置为新节点的文本
        }
      }

    diff.png

    updateChildren(diff算法的体现)

    初始化
        let oldStartIdx = 0 //旧头指针
        let newStartIdx = 0 //新头指针
        let oldEndIdx = oldCh.length - 1  //旧尾指针
        let newEndIdx = newCh.length - 1 //新尾指针
        let oldStartVnode = oldCh[0] //旧头结点
        let oldEndVnode = oldCh[oldEndIdx] //旧尾结点
        let newStartVnode = newCh[0] //新头结点
        let newEndVnode = newCh[newEndIdx]  //新尾结点

    image.png

    四次比较-循环中

    注意: 这里只要能够命中一个,就重开,都不能命中的话再看下一环节, 而不是继续挨个判断image.png

      function updateChildren(){
      ·....
       //好戏从这里开始看
       //只要满足 旧头指针<=旧尾指针 同时  新头指针<= 新尾指针 -- 也可以理解为不能交叉
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        //这里进行一个矫正,是应该在循环的过程中,如果进入key表查询的话复用后会将旧节点置空(后面会说),所以这里会对其进行一个处理
          if (isUndef(oldStartVnode)) {  //旧头结点为空
            oldStartVnode = oldCh[++oldStartIdx] // 往右边走
          } else if (isUndef(oldEndVnode)) {  //旧尾结点为空
            oldEndVnode = oldCh[--oldEndIdx] //往左边走
        //step1
          } else if (sameVnode(oldStartVnode, newStartVnode)) {  //比较旧头和新头,判断是否值得打补丁
            patchVnode(...) //打补丁
            oldStartVnode = oldCh[++oldStartIdx]  //齐头并进向右走
            newStartVnode = newCh[++newStartIdx]  //齐头并进向右走
        //step2
          } else if (sameVnode(oldEndVnode, newEndVnode)) {  //比较旧尾和新尾, 判断是否值得打补丁
            patchVnode(...) //打补丁
            oldEndVnode = oldCh[--oldEndIdx]  //齐头并进向左走
            newEndVnode = newCh[--newEndIdx]  //齐头并进向左走
       //step3
          } else if (sameVnode(oldStartVnode, newEndVnode)) { //比较旧头和新尾,判断是否值得打补丁
            patchVnode(...) //打补丁
            //补完移动节点
            canMove && nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]  //旧头向右走
            newEndVnode = newCh[--newEndIdx] //新尾向左走
        //step4
          } else if (sameVnode(oldEndVnode, newStartVnode)) {  //比较旧尾和新头,判断是否值得打补丁
            patchVnode(...) //打补丁
            //补完移动节点
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]  //旧尾向左走
            newStartVnode = newCh[++newStartIdx] //新头向右走
          }

    实践来一下,就拿上面随机来的例子吧

    image.pngimage.png

    image.pngimage.png

    image.pngimage.png

    image.pngimage.png

    image.pngimage.png

    • 通过上面这个例子,我们把四种情况都命中了一下(一开始随便画的图没想到都命中了哈哈哈),也成功通过复用节点将真实结点变为预期结果,这里便是双端diff一个核心体现了
    • 但是如果四种情况都没有命中的呢(如图下)image.png
    • 则会走向我们最后一个分支,也就是后面介绍的列表寻找image.png
    列表寻找-循环中
    function createKeyToOldIdx(children, beginIdx, endIdx) {
      let i, key
      const map = {}  //初始化一个对象
      for (i = beginIdx; i <= endIdx; ++i) { //从头到尾
        key = children[i].key  //提取每一项的key
        if (isDef(key)) map[key] = i  //key不为空的时候,存入对象,键为key,值为下标
      }
      return map  //返回对象
    }
    //所以该函数的作用其实就是生成了一个节点的键为key,值为下标的一个表
      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  //判断是否有值得打补丁的节点,有则返回
        }
      }
     let oldKeyToIdx, idxInOld, vnodeToMove, refElm;
     ....
    else {
            if (isUndef(oldKeyToIdx))
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) //传入的是旧节点孩子,所以生成了一个旧节点孩子的key表
              //使用三目运算符--- 这里也是要使用key的原因,key有效的话可以通过表获取,无效的话则得进行比遍历比较
            idxInOld = isDef(newStartVnode.key)  //判断新头结点的key是否不为空--是否有效
              ? oldKeyToIdx[newStartVnode.key]  //不为空的的话就到key表寻找该key值对象的旧节点的下标
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) //遍历寻找旧节点数组中是否有和新头结点值得打补丁的节点,有的话则赋值其下标给idxInOld(不通过key)
            if (isUndef(idxInOld)) {  //发现找不到了就直接创建新真实节点
              createElm(...)
            } else { //找到了
              vnodeToMove = oldCh[idxInOld] //找到该下标对应的节点
              if (sameVnode(vnodeToMove, newStartVnode)) { //进行一个比较判断是否值得打补丁
                patchVnode(...) //打补丁 
                oldCh[idxInOld] = undefined  //置空,下次生成表就不会把它加进去
                canMove &&nodeOps.insertBefore( parentElm, vnodeToMove.elm,oldStartVnode.elm ) //移动节点
              } else {
              //不值得打补丁,创建节点
                createElm(...)
              }
            }
            newStartVnode = newCh[++newStartIdx]  //新头指针向前一步走
          }
        } //--- while循环到这里

    也使用一下上面的例子运用一下这个步骤,以下都为key有效的情况image.png

    (重新放一下图,方便看)

    image.pngimage.png

    右图的表中B没有变为undefined是因为表示一开始就生成的,在下次进入循环的时候生成的表才会没有B

    image.pngimage.png

    image.pngimage.png

    image.pngimage.png

    image.pngimage.png

    image.png

    处理
        if (oldStartIdx > oldEndIdx) { //旧的交叉了,说明新增的节点可能还没加上呢
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(....) //新增
        } else if (newStartIdx > newEndIdx) {  //新的交叉了,说明旧节点多余的可能还没删掉呢
          removeVnodes(oldCh, oldStartIdx, oldEndIdx) //把后面那一段删掉
        }

    到这里updateChildren函数就结束喽,自己推导一下节点的变化就会很清晰啦

    (学习视频分享:编程基础视频

    以上就是一文搞懂vue2 diff算法(附图)的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除

    前端(VUE)零基础到就业课程:点击学习

    清晰的学习路线+老师随时辅导答疑

    自己动手写 PHP MVC 框架:点击学习

    快速了解MVC架构、了解框架底层运行原理

    专题推荐:Vue
    上一篇:尤雨溪回应:Vite 真的比 Turbopack 慢 10 倍吗? 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• Vue3如何操作dom?四种方式介绍• vue项目中借助vue-cropper做图片裁剪• 一文聊聊vue中的指令和插值• 浅析Vue中插槽Slot的作用和具名插槽• 在vue中动态的引入图片为什么要使用require?解析分享
    1/1

    PHP中文网