iis服务器助手广告广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >深入了解Vue2中的的双端diff算法
  • 572
分享到

深入了解Vue2中的的双端diff算法

Vue双端diff算法Vue diff算法 2023-02-08 12:02:35 572人浏览 八月长安
摘要

目录简单diff算法更新文本节点key的作用如何移动呢双端diff算法比较方式非理想情况的处理方式今天又重读了Vue2的核心源码,主要之前读vue2源码的时候纯属的硬记,或者说纯粹的

今天又重读了Vue2的核心源码,主要之前读vue2源码的时候纯属的硬记,或者说纯粹的为了应付面试,导致我们并没有去细品源码中的精妙之处。如果回头在重读源码的时候,发现其中有很多地方是值得我们深入了解的。比如我们今天要看的“双端diff”。还记得之前就记得双端diff对比的口诀”头头、尾尾、头尾、尾头“,具体对比做了啥事,这种对比有什么好处,可以说是一无所知。今天我们就来好好的看看。

patch可以将vnode渲染成真实的DOM,实际作用是在现有的DOM进行修改来完成更新视图的目的。

在说“双端diff”之前,我们先来简单看看“简单diff”。

简单diff算法

更新文本节点

const oldVNode = {
  type: "div",
  children: [{ type: "p", children: " 1" }],
  children: [{ type: "p", children: " 2" }],
  children: [{ type: "p", children: " 3" }],
};

const newVNode = {
  type: "div",
  children: [{ type: "p", children: " 4" }],
  children: [{ type: "p", children: " 5" }],
  children: [{ type: "p", children: " 6" }],
};

我们知道,操作DOM的性能开销都比较大,比如我们创建一个DOM的时候,会连带着创建很多的属性。如果我们想将oldVNode替换成newVNode,最暴力的解法就是卸载所有旧子节点,挂载所有新的子节点,这样就会频繁的操作dom。但是我们根据例子发现,如果说节点都是p标签,只是内容发生了改变,那是不是就只可以直接修改内容了,这样就不需要频繁的删除dom,创建dom了。

key的作用

const oldVNode = [{ type: "p" }, { type: "div" }, { type: "span" }];

const newVNode = [{ type: "span" }, { type: "p" }, { type: "div" }];

根据上面的例子,如果操作DOM的话,则需要将旧子节点中的标签和新子节点中的标签进行一对一的对比,如果旧子节点中的{type: 'p'}和新子节点中的{type: 'span'}不是相同的标签,会先卸载{type: 'p'},然后再挂载{ type:'span'},这需要执行 2 次 DOM 操作。仔细观察可以发现,新旧子节点仅仅是顺序不同,这样就可以通过DOM的移动来完成子节点的更新了。

如果仅仅通过type判断,那么type相同,内容不同呢。比如:

const oldVNode = [
  { type: "p", children: " 1" },
  { type: "p", children: " 2" },
  { type: "p", children: " 3" },
];

const newVNode = [
  { type: "p", children: " 3" },
  { type: "p", children: " 1" },
  { type: "p", children: " 2" },
];

这里我们确实可以通过移动DOM来完成更新,但是我们现在继续用type去判断还能行吗?肯定不行的,因为type都是一样的。这时,我们就需要引入额外的key来作为vnode的标识。

const oldVNode = [
  { type: "p", children: " 1", key: " 1" },
  { type: "p", children: " 2", key: " 2" },
  { type: "p", children: " 3", key: " 3" },
];

const newVNode = [
  { type: "p", children: " 3", key: " 3" },
  { type: "p", children: " 1", key: " 1" },
  { type: "p", children: " 2", key: " 2" },
];

这时我们找到需要移动的元素更新即可了。

如何移动呢

比如我们有三个节点,我们希望移动将老的节点更新成为新的节点,此时我们需要一个变量lastIndex为0来记录,只要当前的节点索引小于lastIndex,则说明此节点需要移动。如果当前的索引大于等于lastIndex,则说明此节点不需要移动,并且将当前节点的索引值赋值给lastIndex。

let lastIndex = 0;
for (let i = 0; i < newChildren.length; i++) {
  const newVNode = newChildren[i];
  // 遍历旧的children
  // 在第一场循环中定义变量find,代表是否在旧的一组子节点中找到可复用的节点
  let find = false;
  for (let j = 0; j < oldChildren.length; j++) {
    const oldVNode = oldChildren[j];
    // 如果找到具有相同的key值得两个节点,说明可以复用,仍然需要调用patch函数更新
    if (newVNode.key === oldVNode.key) {
      patch(oldVNode, newVNode, container);
      if (j < lastIndex) {
        // 如果当前找到的节点在旧children中的索引小于最大索引值lastIndex
        // 说明该节点对应的真实DOM需要移动

        // 先获取newVnode的前一个vnode, prevVNode
        const prevVNode = newChildren[i - 1];
        if (prevVNode) {
          // 由于我们要将newVnode对应的真实DOM移动到prevVNode所对应真实DOM后面
          // 所以我们需要获取prevVNode所对应真实DOM的下一个兄弟节点,并将其作为锚点
          const anchor = prevVNode.el.nextSibling;

          // 调用insert方法将newVNode对应的真实DOM插入到锚点元素前面
          // 也就是preVNode对应真实DOM的后面
          insert(newVNode.el, container, anchor);
        }
      } else {
        // 如果当前找到的节点在旧children中的索引不小于最大索引值
        // 则更新lastIndex的值
        lastIndex = j;
      }
      break;
    }

  }
}

以上是一个简单的demo实现,则发现需要对旧子节点移动两次才能更新成新的子节点。

但是我们仔细观察发现,其实只需要将C移动到最前面,这一步就可以实现了。此时我们就需要双端diff算法

双端diff算法

双端diff算法是一种同时对新旧两组子节点的两个断点进行对比的算法。所以我们需要四个索引值,分别指向新旧两组子节点的端点。

比较方式

在双端diff算法比较中,每一轮比较都会分成4个步骤

  • 第一步:旧子节点的开始节点和新子节点的开始节点进行对比,看看他们是否相同。根据他们的标签和key判断,两个节点不相同,所以什么都不做
  • 第二步:旧子节点的结束节点和新子节点的结束节点进行对比,看看他们是否相同。根据他们的标签和key判断,两个节点不相同,所以什么都不做
  • 第三步:旧子节点的开始节点和新子节点的结束节点进行对比,看看他们是否相同。根据他们的标签和key判断,两个节点不相同,所以什么都不做
  • 第四步:旧子节点的结束节点和新子节点的开始节点进行对比,看看他们是否相同。根据他们的标签和key判断,两个节点相同,说明节点可以复用,此时节点需要通过移动来更新。

那又该怎么移动呢?

根据对比我们发现,我们在第四步是将旧子节点的结束节点和新子节点的开始节点进行对比,发现节点可复用。说明节点’D‘在旧子节点中是最后一个节点,在新子节点中是第一个节点,而我们要操作的是老节点也就是现有节点,来实现视图的更新。所以我们只需要将索引 oldEndIdx 指向的虚拟节点所对应的真实DOM 移动到索引 oldStartIdx 指向的虚拟节点所对应的真实 DOM前面。我们看下源码:

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)) { 
        // 老的开始节点 和 新的开始节点一样
       ···
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 老的结束节点 和 新的结束节点一样
       ···
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 老的开始节点 和 新的结束节点一样
       ···
      } 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 {  // 非理想状态下的处理方式
        ···
      }
    } 
  }

从源码中我们看到执行了patchVnode。这个函数其实就是将需要对比的两个新老节点进行打补丁,因为我们此时只能确认新老节点他们的标签和key是一样的,并不代表他们的内容一样,所以需要先更新节点的内容,然后再修改节点的位置。最后我们只需要以头部元素oldStartVNode.elm 作为锚点,将尾部元素 oldEndVNode.elm 移动到锚点前面即可。最后涉及的两个索引分别是oldEndIdxnewStartIdx,所以我们需要更新两者的值,让它们各自朝正确的方向前进一步,并指向下一个节点。

接着继续进行下一轮对比

还是按照我们上面说的那4步对比。此时,当我们执行第二步对比的时候,发现老的结束节点和新的结束节点是一样的。所以就会执行以下操作:

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)) { 
        // 老的开始节点 和 新的开始节点一样
       ···
      } 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
        // 老的开始节点 和 新的结束节点一样
       ···
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老的结束节点 和 新的开始节点一样
       ···
      } else {  // 非理想状态下的处理方式
        ···
      }
    } 
}

这里就只需要通过patchVnode更新新旧子节点的内容,然后更新oldEndIdxnewStartIdx,让它们各自朝正确的方向前进一步,并指向下一个节点。

接着继续进行下一轮对比

当对比到第三步的时候,发现老的开始节点和新的结束节点一样

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)) { 
        // 老的开始节点 和 新的开始节点一样
       ···
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 老的结束节点 和 新的结束节点一样
       ···
      } 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
        // 老的结束节点 和 新的开始节点一样
       ···
      } else {  // 非理想状态下的处理方式
        ···
      }
    } 
}

首先通过patchVnode更新新旧子节点的内容。旧的一组子节点的头部节点与新的一组子节点的尾部节点匹配,则说明该旧节点所对应的真实 DOM 节点需要移动到尾部。因此,我们需要获取当前尾部节点的下一个兄弟节点作为锚点,即 oldEndVNode.el.nextSibling。最后,更新相关索引到下一个位置。

接着继续进行下一轮对比

这里就只需要通过patchVnode更新新旧子节点的内容,发现内容一样什么都不做,然后更新oldEndIdxnewStartIdx,让它们各自朝正确的方向前进一步,并指向下一个节点,这就退出了循环。

以上是理想情况下的处理方式,当然还有非理想情况下的处理方式

非理想情况的处理方式

比如:

此时我们发现之前说的情况都无法命中,所以我们只能通过增加额外的步骤去处理。由于我们都是对比的头部和尾部,既然都无法命中,那就试试非头部、非尾部节点能否复用。此时我们可以发现新子节点中头部节点和旧子节点中的第二个节点是可以复用的,所以只需要将旧子节点中的第二个节点移动到当前旧子节点的头部即可。

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)) { 
        // 老的开始节点 和 新的开始节点一样
       ···
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 老的结束节点 和 新的结束节点一样
       ···
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 老的开始节点 和 新的结束节点一样
       ···
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老的结束节点 和 新的开始节点一样
       ···
      } 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)) { // 如果是相同的节点 则patch 
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 将老的节点中对应的设置为undefined
            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]
      }
    } 
}

首先那新的子节点的头部去旧的一组子节点中寻找,如果没有找到,说明这个节点是一个新的节点,则直接创建节点。如果找到了,通过索引去获取对应的旧节点的信息,如果节点可复用,则需要将当前旧节点移动到头部即可。最后更新新节点的开始节点的索引位置。

到此这篇关于深入了解Vue2中的的双端diff算法的文章就介绍到这了,更多相关Vue双端diff算法内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 深入了解Vue2中的的双端diff算法

本文链接: https://www.lsjlt.com/news/195174.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 深入了解Vue2中的的双端diff算法
    目录简单diff算法更新文本节点key的作用如何移动呢双端diff算法比较方式非理想情况的处理方式今天又重读了vue2的核心源码,主要之前读vue2源码的时候纯属的硬记,或者说纯粹的...
    99+
    2023-02-08
    Vue双端diff算法 Vue diff算法
  • 深入浅析Vue2中的Diff算法
    为什么要用 Diff 算法虚拟 DOM因为 Vue2 底层是用虚拟 DOM 来表示页面结构的,虚拟 DOM其实就是一个对象,如果想知道怎么生成的,其实大概流程就是:首先解析模板字符串,也就是 .vue 文件然后转换成 AST 语法树接着生成...
    99+
    2023-05-14
    diff算法 Vue.js 前端
  • 深入理解vue2中的VNode和diff算法
    虚拟dom和diff算法是vue学习过程中的一个难点,也是面试中必须掌握的一个知识点。这两者相辅相成,是vue框架的核心。今天我们再来总结下vue2中的虚拟dom 和 diff算法。(学习视频分享:vue视频教程)什么是 VNode我们知道...
    99+
    2022-11-22
    VNode diff算法 Vue vue.js
  • Vue3diff算法之双端diff算法详解
    目录双端Diff算法双端比较的原理简单Diff的不足双端Diff介绍Diff流程第一次diff第二次diff第三次diff第四次diff双端Diff的优势非理想情况的处理方式添加新元...
    99+
    2024-04-02
  • 一文详解Vue 的双端 diff 算法
    目录前言diff 算法简单 diff双端 diff总结前言 Vue 和 React 都是基于 vdom 的前端框架,组件渲染会返回 vdom,渲染器再把 vdom 通过增删改的 ap...
    99+
    2024-04-02
  • 怎么深入解析Vue3中的diff 算法
    今天给大家介绍一下怎么深入解析Vue3中的diff 算法。文章的内容小编觉得不错,现在给大家分享一下,觉得有需要的朋友可以了解一下,希望对大家有所帮助,下面跟着小编的思路一起来阅读吧。1.0  diff 无key子节点在处理被标记...
    99+
    2023-06-26
  • Vue中的双端diff算法怎么应用
    这篇文章主要讲解了“Vue中的双端diff算法怎么应用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Vue中的双端diff算法怎么应用”吧!Vue 和 React 都是基于 vdom 的前端...
    99+
    2023-07-02
  • Vue2中的Diff算法怎么使用
    这篇文章主要介绍了Vue2中的Diff算法怎么使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue2中的Diff算法怎么使用文章都会有所收获,下面我们一起来看看吧。为什么要用 Diff 算法虚拟 DOM因为...
    99+
    2023-07-05
  • 深入浅析React中diff算法
    React中diff算法的理解 diff算法用来计算出Virtual DOM中改变的部分,然后针对该部分进行DOM操作,而不用重新渲染整个页面,渲染整个DOM结构的过程中开销是很大的...
    99+
    2024-04-02
  • Vue的双端diff算法怎么实现
    这篇文章主要介绍了Vue的双端diff算法怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue的双端diff算法怎么实现文章都会有所收获,下面我们一起来看看吧。前言Vue 和 React 都是基于 vd...
    99+
    2023-07-02
  • Vue2 的 diff 算法规则原理详解
    目录前言算法规则diff 优化策略老数组的开始与新数组的开始老数组的结尾与新数组的结尾老数组的开始与新数组的结尾老数组的结尾与新数组的开始以上四种情况都没对比成功推荐在渲染列表时为节...
    99+
    2024-04-02
  • vue2的diff算法怎么使用
    这篇文章主要介绍“vue2的diff算法怎么使用”,在日常操作中,相信很多人在vue2的diff算法怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”vue2的diff算法怎么使用”的疑惑有所帮助!接下来...
    99+
    2023-07-04
  • vue2中的VNode和diff算法怎么使用
    本文小编为大家详细介绍“vue2中的VNode和diff算法怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“vue2中的VNode和diff算法怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。什么是...
    99+
    2023-07-04
  • React中的Diff算法你了解吗
    目录一、Diff算法的作用二、React的Diff算法  1、什么是调和?2、什么是React diff算法?3、diff策略4、tree diff:5、comp...
    99+
    2024-04-02
  • 怎样深入理解vue中的虚拟DOM和Diff算法
    怎样深入理解vue中的虚拟DOM和Diff算法,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。真实DOM的渲染在讲虚拟DOM之前,先说一下真实DOM的渲染。浏览器真实DOM渲...
    99+
    2023-06-22
  • React Diff算法不采用Vue的双端对比原因详解
    目录前言React 官方的解析Fiber 的结构Fiber 链表的生成React 的 Diff 算法第一轮,常见情况的比对第二轮,不常见的情况的比对重点如何协调更新位置信息小结图文解...
    99+
    2024-04-02
  • Vue的diff算法原理你真的了解吗
    目录思维导图0. 从常见问题引入1. 生成虚拟dom1. h方法实现2. render方法实现3. 再次渲染2. diff算法1. 对常见的dom做优化情况1:末尾追加一个元素(头和...
    99+
    2024-04-02
  • 深入了解vue2与vue3的生命周期对比
    目录周期对比用法总结 周期对比 vue2 vue3 ...
    99+
    2024-04-02
  • Vue的虚拟DOM和diff算法你了解吗
    目录什么是虚拟DOM?为什么需要虚拟DOM?总结在vue 中 数据改变 -> 虚拟DOM(计算变更)-> 操作DOM -> 视图更新 虚拟DOM: js执行速度比较...
    99+
    2024-04-02
  • 深入了解Golang中的run方法
    Go是一种快速,可靠和开源的编程语言。Go语言通过其高效的并发性和垃圾回收器以及C的速度,用于构建高效和可扩展的网络服务器和系统编程。让我们深入了解Golang中的run方法。run()方法是golang中重要的一种方法,可以用于创建新的协...
    99+
    2023-05-14
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作