iis服务器助手广告
返回顶部
首页 > 资讯 > 精选 >Vue的双端diff算法怎么实现
  • 350
分享到

Vue的双端diff算法怎么实现

2023-07-02 13:07:15 350人浏览 薄情痞子
摘要

这篇文章主要介绍了Vue的双端diff算法怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue的双端diff算法怎么实现文章都会有所收获,下面我们一起来看看吧。前言Vue 和 React 都是基于 vd

这篇文章主要介绍了Vue的双端diff算法怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue的双端diff算法怎么实现文章都会有所收获,下面我们一起来看看吧。

前言

Vue 和 React 都是基于 vdom 的前端框架,组件渲染会返回 vdom,渲染器再把 vdom 通过增删改的 api 同步到 dom。

当再次渲染时,会产生新的 vdom,渲染器会对比两棵 vdom 树,对有差异的部分通过增删改的 api 更新到 dom。

这里对比两棵 vdom 树,找到有差异的部分的算法,就叫做 diff 算法。

diff 算法

我们知道,两棵树做 diff,复杂度是 O(n^3) 的,因为每个节点都要去和另一棵树的全部节点对比一次,这就是 n 了,如果找到有变化的节点,执行插入、删除、修改也是 n 的复杂度。所有的节点都是这样,再乘以 n,所以是 O(n * n * n) 的复杂度。

Vue的双端diff算法怎么实现

这样的复杂度对于前端框架来说是不可接受的,这意味着 1000 个节点,渲染一次就要处理 1000 * 1000 * 1000,一共 10 亿次。

所以前端框架的 diff 约定了两种处理原则:只做同层的对比,type 变了就不再对比子节点。

因为 dom 节点做跨层级移动的情况还是比较少的,一般情况下都是同一层级的 dom 的增删改。

这样只要遍历一遍,对比一下 type 就行了,是 O(n) 的复杂度,而且 type 变了就不再对比子节点,能省下一大片节点的遍历。另外,因为 vdom 中记录了关联的 dom 节点,执行 dom 的增删改也不需要遍历,是 O(1)的,整体的 diff 算法复杂度就是 O(n) 的复杂度。

Vue的双端diff算法怎么实现

1000 个节点渲染一次最多对比 1000 次,这样的复杂度就是可接受的范围了。但是这样的算法虽然复杂度低了,却还是存在问题的。

比如一组节点,假设有 5 个,类型是 ABCDE,下次渲染出来的是 EABCD,这时候逐一对比,发现 type 不一样,就会重新渲染这 5 个节点。

而且根据 type 不同就不再对比子节点的原则,如果这些节点有子节点,也会重新渲染。dom 操作是比较慢的,这样虽然 diff 的算法复杂度是低了,重新渲染的性能也不高。

所以,diff 算法除了考虑本身的时间复杂度之外,还要考虑一个因素:dom 操作的次数。

上面那个例子的 ABCDE 变为 EABCD,很明显只需要移动一下 E 就行了,根本不用创建新元素。

但是怎么对比出是同个节点发生了移动呢?

判断 type 么? 那不行,同 type 的节点可能很多,区分不出来的。最好每个节点都是有唯一的标识。

所以当渲染一组节点的时候,前端框架会让开发者指定 key,通过 key 来判断是不是有点节点只是发生了移动,从而直接复用。这样,diff 算法处理一组节点的对比的时候,就要根据 key 来再做一次算法的优化。我们会把基于 key 的两组节点的 diff 算法叫做多节点 diff 算法,它是整个 vdom 的 diff 算法的一部分。

接下来我们来学习一下多节点 diff 算法:

简单 diff

假设渲染 ABCD 一组节点,再次渲染是 DCAB,这时候怎么处理呢?

多节点 diff 算法的目的是为了尽量复用节点,通过移动节点代替创建。

所以新 vnode 数组的每个节点我们都要找下在旧 vnode 数组中有没有对应 key 的,有的话就移动到新的位置,没有的话再创建新的。

也就是这样的:

const oldChildren = n1.childrenconst newChildren = n2.childrenlet lastIndex = 0// 遍历新的 childrenfor (let i = 0; i < newChildren.length; i++) {    const newVNode = newChildren[i]    let j = 0    let find = false    // 遍历旧的 children    for (j; j < oldChildren.length; j++) {      const oldVNode = oldChildren[j]      // 如果找到了具有相同 key 值的两个节点,则调用 patch 函数更新      if (newVNode.key === oldVNode.key) {        find = true        patch(oldVNode, newVNode, container)        处理移动...        break //跳出循环,处理下一个节点      }   }   // 没有找到就是新增了   if (!find) {      const prevVNode = newChildren[i - 1]      let anchor = null      if (prevVNode) {        anchor = prevVNode.el.nextSibling      } else {        anchor = container.firstChild      }      patch(null, newVNode, container, anchor)   }}

这里的 patch 函数的作用是更新节点的属性,重新设置事件监听器。如果没有对应的旧节点的话,就是插入节点,需要传入一个它之后的节点作为锚点 anchor。

我们遍历处理新的 vnode:

先从旧的 vnode 数组中查找对应的节点,如果找到了就代表可以复用,接下来只要移动就好了。如果没找到,那就执行插入,锚点是上一个节点的 nextSibling。

Vue的双端diff算法怎么实现

那如果找到了可复用的节点之后,那移动到哪里呢?其实新的 vnode 数组中记录的顺序就是目标的顺序。所以把对应的节点按照新 vnode 数组的顺序来移动就好了

const prevVNode = newChildren[i - 1]if (prevVNode) {    const anchor = prevVNode.el.nextSibling    insert(newVNode.el, container, anchor)}

要插入到 i 的位置,那就要取 i-1 位置的节点的 nextSibling 做为锚点来插入当前节点。

Vue的双端diff算法怎么实现

但是并不是所有的节点都需要移动,比如处理到第二个新的 vnode,发现它在旧的 vnode 数组中的下标为 4,说明本来就是在后面了,那就不需要移动了。反之,如果是 vnode 查找到的对应的旧的 vnode 在当前 index 之前才需要移动。

也就是这样:

let j = 0let find = false// 遍历旧的 childrenfor (j; j < oldChildren.length; j++) {    const oldVNode = oldChildren[j]    // 如果找到了具有相同 key 值的两个节点,则调用 patch 函数更新之    if (newVNode.key === oldVNode.key) {        find = true        patch(oldVNode, newVNode, container)        if (j < lastIndex) { // 旧的 vnode 数组的下标在上一个 index 之前,需要移动          const prevVNode = newChildren[i - 1]          if (prevVNode) {            const anchor = prevVNode.el.nextSibling            insert(newVNode.el, container, anchor)          }        } else {// 不需要移动          // 更新 lastIndex          lastIndex = j        }        break    }}

查找新的 vnode 在旧的 vnode 数组中的下标,如果找到了的话,说明对应的 dom 就是可以复用的,先 patch 一下,然后移动。

移动的话判断下下标是否在 lastIndex 之后,如果本来就在后面,那就不用移动,更新下 lastIndex 就行。如果下标在 lastIndex 之前,说明需要移动,移动到的位置前面分析过了,就是就是新 vnode 数组 i-1 的后面。这样,我们就完成了 dom 节点的复用和移动。

新的 vnode 数组全部处理完后,旧的 vnode 数组可能还剩下一些不再需要的,那就删除它们:

// 遍历旧的节点for (let i = 0; i < oldChildren.length; i++) {    const oldVNode = oldChildren[i]    // 拿着旧 VNode 去新 children 中寻找相同的节点    const has = newChildren.find(      vnode => vnode.key === oldVNode.key    )    if (!has) {      // 如果没有找到相同的节点,则移除      unmount(oldVNode)    }}

这样,我们就完成了两组 vnode 的 diff 和对应 dom 的增删改。

小结一下:

diff 算法的目的是根据 key 复用 dom 节点,通过移动节点而不是创建新节点来减少 dom 操作。

对于每个新的 vnode,在旧的 vnode 中根据 key 查找一下,如果没查找到,那就新增 dom 节点,如果查找到了,那就可以复用。

复用的话要不要移动要判断下下标,如果下标在 lastIndex 之后,就不需要移动,因为本来就在后面,反之就需要移动。

最后,把旧的 vnode 中在新 vnode 中没有的节点从 dom 树中删除

这就是一个完整的 diff 算法的实现:

Vue的双端diff算法怎么实现

这个 diff 算法我们是从一端逐个处理的,叫做简单 diff 算法。简单 diff 算法其实性能不是最好的,比如旧的 vnode 数组是 ABCD,新的 vnode 数组是 DABC,按照简单 diff 算法,A、B、C 都需要移动。

那怎么优化这个算法呢?从一个方向顺序处理会有这个问题,那从两个方向同时对比呢?

这就是双端 diff 算法:

双端 diff

简单 diff 算法能够实现 dom 节点的复用,但有的时候会做一些没必要的移动。双端 diff 算法解决了这个问题,它是从两端进行对比。

我们需要 4 个指针,分别指向新旧两个 vnode 数组的头尾:

Vue的双端diff算法怎么实现

头和尾的指针向中间移动,直到 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx,说明就处理完了全部的节点。

每次对比下两个头指针指向的节点、两个尾指针指向的节点,头和尾指向的节点,是不是 key是一样的,也就是可复用的。如果是可复用的话就直接用,调用 patch 更新一下,如果是头尾这种,还要移动下位置。

也就是这样的:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {  if (oldStartVNode.key === newStartVNode.key) { // 头头    patch(oldStartVNode, newStartVNode, container)    oldStartVNode = oldChildren[++oldStartIdx]    newStartVNode = newChildren[++newStartIdx]  } else if (oldEndVNode.key === newEndVNode.key) {//尾尾    patch(oldEndVNode, newEndVNode, container)    oldEndVNode = oldChildren[--oldEndIdx]    newEndVNode = newChildren[--newEndIdx]  } else if (oldStartVNode.key === newEndVNode.key) {//头尾,需要移动    patch(oldStartVNode, newEndVNode, container)    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)    oldStartVNode = oldChildren[++oldStartIdx]    newEndVNode = newChildren[--newEndIdx]  } else if (oldEndVNode.key === newStartVNode.key) {//尾头,需要移动    patch(oldEndVNode, newStartVNode, container)    insert(oldEndVNode.el, container, oldStartVNode.el)    oldEndVNode = oldChildren[--oldEndIdx]    newStartVNode = newChildren[++newStartIdx]  } else {    // 头尾没有找到可复用的节点  }}

头头和尾尾的对比比较简单,头尾和尾头的对比还要移动下节点。比如旧 vnode 的头节点是新的 vnode 的尾节点,那就要把它移动到旧的 vnode 的尾节点的位置。

也就是:

insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)

插入节点的锚点节点是 oldEndVNode 对应的 dom 节点的 nextSibling。如果旧 vnode 的尾节点是新 vnode 的头结点,那就要把它移动到旧 vnode 的头结点的位置。

也就是:

insert(oldEndVNode.el, container, oldStartVNode.el)

插入节点的锚点节点是 oldStartVNode 对应的 dom 节点(因为要插在它之前)。从双端进行对比,能尽可能的减少节点移动的次数。

当然,还要处理下如果双端都没有可复用节点的情况:

如果双端都没有可复用节点,那就在旧节点数组中找,找到了就把它移动过来,并且原位置置为 undefined。没找到的话就插入一个新的节点。

也就是这样:

const idxInOld = oldChildren.findIndex(  node => node.key === newStartVNode.key)if (idxInOld > 0) {  const vnodeToMove = oldChildren[idxInOld]  patch(vnodeToMove, newStartVNode, container)  insert(vnodeToMove.el, container, oldStartVNode.el)  oldChildren[idxInOld] = undefined} else {  patch(null, newStartVNode, container, oldStartVNode.el)}

因为有了一些 undefined 的节点,所以要加上空节点的处理逻辑:

if (!oldStartVNode) {    oldStartVNode = oldChildren[++oldStartIdx]} else if (!oldEndVNode) {    oldEndVNode = newChildren[--oldEndIdx]}

这样就完成了节点的复用和移动的逻辑。那确实没有可复用的节点的那些节点呢?

经过前面的移动之后,剩下的节点都被移动到了中间,如果新 vnode 有剩余,那就批量的新增,如果旧 vnode 有剩余那就批量的删除。因为前面一个循环的判断条件是 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,这样如果 old vnode 多了,最后 newStartIdx 会小于 newEndIdx。如果 new vnode 多了,最后 oldStartIdx 会小于 oldEndIdx。

所以判断条件是这样的:

if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {  // 添加新节点  for (let i = newStartIdx; i <= newEndIdx; i++) {    patch(null, newChildren[i], container, oldStartVNode.el)  }} else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {  // 移除操作  for (let i = oldStartIdx; i <= oldEndIdx; i++) {    unmount(oldChildren[i])  }}

这样就是一个完整的 diff 算法了,包括查找可复用节点和移动节点、新增和删除节点。而且因为从两侧查找节点,会比简单 diff 算法性能更好一些。

比如 ABCD 到 DABC,简单 diff 算法需要移动 ABC 三个节点,而双端 diff 算法只需要移动 D 一个节点。

小结一下:

双端 diff 是头尾指针向中间移动的同时,对比头头、尾尾、头尾、尾头是否可以复用,如果可以的话就移动对应的 dom 节点。

如果头尾没找到可复用节点就遍历 vnode 数组来查找,然后移动对应下标的节点到头部。最后还剩下旧的 vnode 就批量删除,剩下新的 vnode 就批量新增

Vue的双端diff算法怎么实现

双端 diff 算法是 Vue2 采用的 diff 算法,性能还不错。后来,vue3 又对 diff 算法进行了一次升级,叫做快速 diff 算法。这个后面再讲。

关于“Vue的双端diff算法怎么实现”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“Vue的双端diff算法怎么实现”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注编程网精选频道。

--结束END--

本文标题: Vue的双端diff算法怎么实现

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

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

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

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

下载Word文档
猜你喜欢
  • Vue的双端diff算法怎么实现
    这篇文章主要介绍了Vue的双端diff算法怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue的双端diff算法怎么实现文章都会有所收获,下面我们一起来看看吧。前言Vue 和 React 都是基于 vd...
    99+
    2023-07-02
  • Vue中的双端diff算法怎么应用
    这篇文章主要讲解了“Vue中的双端diff算法怎么应用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Vue中的双端diff算法怎么应用”吧!Vue 和 React 都是基于 vdom 的前端...
    99+
    2023-07-02
  • 一文详解Vue 的双端 diff 算法
    目录前言diff 算法简单 diff双端 diff总结前言 Vue 和 React 都是基于 vdom 的前端框架,组件渲染会返回 vdom,渲染器再把 vdom 通过增删改的 ap...
    99+
    2024-04-02
  • vue怎么实现diff算法
    这篇文章主要介绍“vue怎么实现diff算法”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“vue怎么实现diff算法”文章能帮助大家解决问题。 模块路径:src\...
    99+
    2024-04-02
  • Vue3diff算法之双端diff算法详解
    目录双端Diff算法双端比较的原理简单Diff的不足双端Diff介绍Diff流程第一次diff第二次diff第三次diff第四次diff双端Diff的优势非理想情况的处理方式添加新元...
    99+
    2024-04-02
  • Vue 2.5中怎么实现一个Diff算法
    Vue 2.5中怎么实现一个Diff算法,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1.VNode对象一个VNode的实例包含了以下属性,这...
    99+
    2024-04-02
  • 深入了解Vue2中的的双端diff算法
    目录简单diff算法更新文本节点key的作用如何移动呢双端diff算法比较方式非理想情况的处理方式今天又重读了vue2的核心源码,主要之前读vue2源码的时候纯属的硬记,或者说纯粹的...
    99+
    2023-02-08
    Vue双端diff算法 Vue diff算法
  • React Diff算法不采用Vue的双端对比原因详解
    目录前言React 官方的解析Fiber 的结构Fiber 链表的生成React 的 Diff 算法第一轮,常见情况的比对第二轮,不常见的情况的比对重点如何协调更新位置信息小结图文解...
    99+
    2024-04-02
  • React怎么实现核心Diff算法
    本篇内容主要讲解“React怎么实现核心Diff算法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“React怎么实现核心Diff算法”吧!Diff算法的设计思路试想,Diff算法需要考虑多少种情...
    99+
    2023-06-30
  • Vue的diff算法原理是什么
    这篇文章将为大家详细讲解有关Vue的diff算法原理是什么,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。思维导图0. 从常见问题引入虚拟dom是什么如何创建虚拟dom虚拟dom如何渲染成真是dom虚拟do...
    99+
    2023-06-29
  • 怎么在react中实现一个diff算法
    这期内容当中小编将会给大家带来有关怎么在react中实现一个diff算法,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。单节点Diff单节点Diff比较简单,只有key相同并且type相同的情况才会尝试复用...
    99+
    2023-06-14
  • Vue中的虚拟DOM和Diff算法实例分析
    这篇文章主要介绍了Vue中的虚拟DOM和Diff算法实例分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue中的虚拟DOM和Diff算法实例分析文章都会有所收获,下面我们一起来看看吧。简单介绍一下 虚拟DO...
    99+
    2023-06-29
  • 简单谈谈Vue中的diff算法
    目录概述 虚拟Dom(virtual dom) 原理 实现过程 patch方法 sameVnode函数 patchVnode函数 updateChildren函数 结语 概述 di...
    99+
    2024-04-02
  • vue2的diff算法怎么使用
    这篇文章主要介绍“vue2的diff算法怎么使用”,在日常操作中,相信很多人在vue2的diff算法怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”vue2的diff算法怎么使用”的疑惑有所帮助!接下来...
    99+
    2023-07-04
  • vue3 diff算法怎么应用
    这篇文章主要介绍“vue3 diff算法怎么应用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“vue3 diff算法怎么应用”文章能帮助大家解决问题。一、可能性(常见):旧的:a...
    99+
    2023-07-02
  • Vue2 diff算法怎么掌握
    今天小编给大家分享一下Vue2 diff算法怎么掌握的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。什么是 diff 在我的理...
    99+
    2023-07-05
  • C++图搜索算法之双端队列广搜怎么实现
    这篇文章主要介绍“C++图搜索算法之双端队列广搜怎么实现”,在日常操作中,相信很多人在C++图搜索算法之双端队列广搜怎么实现问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C++图搜索算法之双端队列广搜怎么实现...
    99+
    2023-07-02
  • Vue2中的Diff算法怎么使用
    这篇文章主要介绍了Vue2中的Diff算法怎么使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue2中的Diff算法怎么使用文章都会有所收获,下面我们一起来看看吧。为什么要用 Diff 算法虚拟 DOM因为...
    99+
    2023-07-05
  • React实现核心Diff算法的示例代码
    目录Diff算法的设计思路Demo介绍Diff算法实现遍历前的准备工作遍历after遍历后的收尾工作总结Diff算法的设计思路 试想,Diff算法需要考虑多少种情况呢?大体分三种,分...
    99+
    2024-04-02
  • react跟vue的diff算法有哪些区别
    这篇“react跟vue的diff算法有哪些区别”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作