文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2022-11-22 23:42

关注

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算法(附图)的详细内容,更多请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯