返回顶部
首页 > 资讯 > 前端开发 > JavaScript >Vue3diff算法的简单解刨
  • 434
分享到

Vue3diff算法的简单解刨

Vue3diff算法Vuediff算法Vue3diff 2023-02-09 09:02:39 434人浏览 泡泡鱼
摘要

目录性能比较前置与后置的预处理节点无序最长递增子序列上篇我们介绍了Vue2中的双端diff算法的优势(相比于简单算法相同场景下移动DOM次数更少)。如今vue3的势头正盛,在diff

上篇我们介绍了Vue2中的双端diff算法的优势(相比于简单算法相同场景下移动DOM次数更少)。如今vue3的势头正盛,在diff算法方面也做了相应的变化,利用到了最长递增子序列把性能又提升了一个档次。对于技术栈使用Vue的同学来说又是必须要学习的一部分。

性能比较

此图借鉴了《Vuejs设计与实现》这本书

iviinferno所采用的快速diff算法的性能要稍优于Vue2的双端diff算法。既然快速diff算法这么高效,我们当然有必要了解它咯。

前置与后置的预处理

这里有两组子节点,c1表示老的子节点,c2表示新的子节点。首先将从c1c2的头部节点开始对比,如果节点相同则通过patch更新节点的内容。e1表示老的子节点的尾部索引e2表示新的子节点的尾部索引。

while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as Vnode)
        : nORMalizeVNode(c2[i]))
        // 如果节点相同 则进行递归执行patch更新节点
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // 如果节点不相同,则直接退出
        break
      }
      i++
}

然后将索引递增,发现c1中节点B和c2中节点D不是同一节点,则循环退出。接着将从c1c2的尾部节点开始对比,如果节点相同则通过patch更新节点的内容。

while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
        // 如果是相同节点 则递归执行patch
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // 如果节点不相同 则退出
        break
      }
      e1--
      e2--
}

此过程经历了c1的节点C和c2的节点C对比,c1的节点B和c2的节点B对比,节点相同则通过patch更新节点的内容,每次循环e1 e2向前推进。当循环到了c1中节点B和c2中节点D不是同一节点,则循环退出

此时当新节点中还有剩余,则需要添加新节点。 相反的,如果旧节点有剩余需要删除旧节点

if (i > e1) {
      // 当索引大于老节点的尾部
      if (i <= e2) {
        // 当索引小于新节点的尾部 需要将剩余的节点添加
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch(
            null,
            (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          i++
        }
      }
}

else if (i > e2) {
      // 当索引i大于尾部索引e2 则直接删除旧子树从索引i开始到e1部分的节点
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
      }
}

上面的情况是有序的,下面我们看下无序的情况

节点无序

上图是经过上面的两层循环之后的结果。我们首先看下代码,由于节点无序情况的代码比较长,我们一段段的解刨

const s1 = i // prev starting index  旧子序列开始索引 从i开始记录
const s2 = i // next starting index  新子序列开始索引 从i开始记录

// 5.1 build key:index map for newChildren
// 根据key 建立新子序列的索引图
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
    // 获取新节点
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    if (nextChild.key != null) {
      if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
        warn(
          `Duplicate keys found during update:`,
          JSON.stringify(nextChild.key),
          `Make sure keys are unique.`
        )
      }
      //  根据新节点的key 和 对应的索引做映射关系
      // <key, index>
      keyToNewIndexMap.set(nextChild.key, i)
    }
}
let j
// 新子序列已经更新的节点数量
let patched = 0
// 新子序列待更新节点的数量,等于新子序列的长度
const toBePatched = e2 - s2 + 1
// 是否存在需要移动的节点
let moved = false
// 用于跟踪判断是否有节点需要移动的节点
let maxNewIndexSoFar = 0
// 这个数组存储新子序列中的元素在旧子序列节点出的索引,用于确定最长递增子序列
const newIndexToOldIndexMap = new Array(toBePatched)
// 初始化数组 为0
// 0是一个特殊的值,如果遍历之后仍有元素的值为0,则说明这个新节点没有对应的旧节点
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

s1表示旧子节点开始遍历的索引,从i开始记录,s2表示新子节点开始遍历的索引,也从i开始记录。根据新子节点的key建立新子序列的索引图keyToNewIndexMappatched表示新子序列已经更新的节点数量,toBePatched表示新子序列待更新节点的数量,等于新子序列的长度。moved表示是否存在需要移动的节点。maxNewIndexSoFar用于跟踪判断是否有节点需要移动的节点。newIndexToOldIndexMap存储新子序列中的节点在旧子序列节点的索引,用于确定最长递增子序列,初始化全部为0,0是一个特殊的值,如果遍历之后仍有元素的值为0,则说明这个新节点没有对应的旧节点。我们在图中表示一下

然后正序遍历旧子序列

// 正序遍历旧子序列
for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]
    if (patched >= toBePatched) {
      // 所有新的子序列节点都已经更新,删除剩余的节点
      // all new children have been patched so this can only be a removal
      unmount(prevChild, parentComponent, parentSuspense, true)
      continue
    }

    let newIndex
    if (prevChild.key != null) {
      // 查找旧子序列中的节点在新子序列中的索引
      newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
      // key-less node, try to locate a key-less node of the same type
      for (j = s2; j <= e2; j++) {
        if (
          newIndexToOldIndexMap[j - s2] === 0 &&
          isSameVNodeType(prevChild, c2[j] as VNode)
        ) {
          newIndex = j
          break
        }
      }
    }
    if (newIndex === undefined) {
      // 找不到说明旧子序列需要删除
      unmount(prevChild, parentComponent, parentSuspense, true)
    } else {
      // 更新新子序列中的元素在旧子序列中的索引,这里 +1 偏移是为了避免i为0的特殊情况 影响后面最长递增子序列的求解
      newIndexToOldIndexMap[newIndex - s2] = i + 1
      //maxNewIndexSoFar 存储的是上次旧值的newIndex 如果不是一直递增 则说明有移动 
      if (newIndex >= maxNewIndexSoFar) {
        maxNewIndexSoFar = newIndex
      } else {
        moved = true
      }
      patch(
        prevChild,
        c2[newIndex] as VNode,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized处
      )
      patched++
    }
}

每次遍历拿到旧子节点的值prevChild,如果patched >= toBePatched也就是说新子序列已经更新的节点数量大于等于待更新的节点数量,说明新子序列的所有节点更新完毕,旧子序列中剩余的节点删除即可。

如果每次循环拿到的旧子节点的值的key存在,则查找旧子序列中的节点在新子序列中的索引标记为newIndex。如果key不存在,则遍历新子序列待更新的节点,如果prevChild和遍历新子序列待更新的节点有相同的节点,则将索引赋值给newIndex。接着判断newIndex是否存在,如果不存在,则说明新子序列中找不到旧子序列中的这个节点,则直接删除。如果存在,则更新新子序列中的元素在旧子序列中的索引,这里 ‘+1’ 偏移是为了避免i为0的特殊情况,影响后面最长递增子序列的求解,因为我们现在规定的newIndexToOldIndexMap中的元素为0说明这个元素没有对应的老节点,如果不'+1'我们避免不了i为0的情况,这样就影响了最长递增子序列的求解。我们看下newIndexToOldIndexMap重新赋值之后的结果

maxNewIndexSoFar存储的是上次旧值的newIndex,如果不是一直递增 则说明有移动,则moved设置为ture。然后将newIndex对应的新节点和prevChild老节点进行patch,然后patched++记录更新的次数。

我们继续看下面的代码

const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// 倒序遍历 以便使用更新的节点作为锚点
for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i
    const nextChild = c2[nextIndex] as VNode
    // 锚点指向上一个更新的节点, 如果nextIndex超过新节点的长度,则指向parentAnchor
    const anchor =
      nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
    if (newIndexToOldIndexMap[i] === 0) {
      // mount new
      // 挂载新节点
      patch(
        null,
        nextChild,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else if (moved) {
      // 没有最长递增子序列或者当前节点索引不在最长递增子序列中, 需要移动
      if (j < 0 || i !== increasingNewIndexSequence[j]) {
        move(nextChild, container, anchor, MoveType.REORDER)
      } else {
        j--
      }
    }
}

首先判断是否需要移动,如果需要移动怎么才能以移动最少的步数完成更新呢?这就需要用到最长递增子序列(getSequence这个方法代码我们贴到文章最后)。我们得到了最长递增子序列的索引值的集合increasingNewIndexSequence,我们看下结果。

根据结果我们发现在newIndexToOldIndexMap中只有索引为0和1的节点是递增的,所以只有这两个对应的旧的子序列的节点是不需要移动的,其他的则需要移动。

倒序遍历。为什么要倒序遍历呢?因为我们将节点插入到已经更新的节点前面(从后往前遍历可以始终保持当前遍历的节点的下一个节点是更新过的)这里使用的是insertBefore

比如这个例子节点‘G’就要插入到节点‘E’的前面,节点‘E’是已经更新过的了。

继续往前推进,节点‘B’不在最长递增子序列increasingNewIndexSequence中,所以需要移动。然后拿到节点‘B’对应的el插入到节点‘G’的前面。这个节点‘B’的el我们从节点‘B’的Vnode上面就可以获取到了。因为当两个新旧节点进行对比的时候会进行下面的操作

以上就是我们要介绍的Vue3的diff算法的核心内容

总结

有序的情况比较简单,我们就直接说无序的情况。

1.根据key建立新子序列的索引图 keyToNewIndexMap

通过遍历新子序列的节点 将keyindex映射

2.根据新子序列中待更新节点的数量toBePatched创建数组newIndexToOldIndexMap数组初始化为0

这个数组就是保存新子序列中的节点在旧子序列中的索引位置。

3.遍历旧子节点 拿旧的子节点去keyToNewIndexMap中找对应新子节点的位置

  • 如果找不到 则说明这个节点在新的子节点中没有则删除
  • 找到了之后则更新newIndexToOldIndexMap,数组中的元素被重新赋值为新子序列中的节点在旧子序列中的索引位置,为0的元素说明这个节点是新增的。
  • 将新旧子节点对比更新

4.通过最长递增子序列找到那些节点是需要移动的,哪些节点是不需要的移动的

最长递增子序列

// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr: number[]): number[] {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    // arrI 为当前顺序取出的元素
    const arrI = arr[i]
    // 排除0的情况
    if (arrI !== 0) {
      // result 存储的是长度为i的递增子序列最小末尾值的索引 
      j = result[result.length - 1]
      // arr[j]为末位置,如果满足arr[j]<arrI 那么直接在当前递增子序列后面添加
      if (arr[j] < arrI) {
        // 存储result更新前的最后一个索引的值
        p[i] = j
        // 存储元素对应的索引值
        result.push(i)
        continue
      }
      // 不满足 则执行二分搜索
      u = 0
      v = result.length - 1
      // 查找第一个比arrI小的节点,更新result的值
      while (u < v) {
        // c记录中间的位置
        c = (u + v) >> 1
        if (arr[result[c]] < arrI) {
          // 若中间的值小于arrI 则在后边 更新下沿
          u = c + 1
        } else {
          // 更新下沿
          v = c
        }
      }
      // 找到第一个比arrI小的位置u,插入它
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

以上就是Vue3 diff算法的简单解刨的详细内容,更多关于Vue3 diff算法的资料请关注编程网其它相关文章!

--结束END--

本文标题: Vue3diff算法的简单解刨

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

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

猜你喜欢
  • Vue3diff算法的简单解刨
    目录性能比较前置与后置的预处理节点无序最长递增子序列上篇我们介绍了vue2中的双端diff算法的优势(相比于简单算法相同场景下移动DOM次数更少)。如今Vue3的势头正盛,在diff...
    99+
    2023-02-09
    Vue3 diff算法 Vue diff算法 Vue3 diff
  • Vue3diff算法之双端diff算法详解
    目录双端Diff算法双端比较的原理简单Diff的不足双端Diff介绍Diff流程第一次diff第二次diff第三次diff第四次diff双端Diff的优势非理想情况的处理方式添加新元...
    99+
    2024-04-02
  • 简单的排序算法
    现在的IT行业并不像以前那么好混了,从业人员过多,导致初级程序员过剩,这也间接导致了公司的招聘门槛越来越高,要求程序员掌握的知识也越来越多。算法也是一个争论了很久的话题,程序员到底该不该掌握算法?不同的人有不同的答案,而事实上,很多公司都对...
    99+
    2021-12-20
    java教程 java
  • 如何简单解释 MapReduce 算法
    今天就跟大家聊聊有关如何简单解释 MapReduce 算法,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。在Hackbright做导师期间,我被要求向技术背景有限的学生解释MapRed...
    99+
    2023-06-17
  • C++并查集算法简单详解
    目录1、并查集的初始化2、并查集的查找操作3、并查集的合并操作4、为什么要路径压缩?5、实现路径压缩总结1、并查集的初始化 并查集是用一个数组实现的。首先先定义一个数组: int f...
    99+
    2024-04-02
  • PostGis的几个简单应用算法
    2.根据点在数据库里查询在哪个多边形中 SELECT id, geom from dt_cy where ST_Contains(geom, st_geometryfromtext("POINT(113.4587...
    99+
    2020-07-23
    PostGis的几个简单应用算法
  • RSA加密算法的简单案例
    RSA加密算法是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知的绝大多数密码***。那关于RSA加密算法有哪些应用呢?以下举一个数据库身份验证的案例。在使用数据集进行身份认证时,密码存在数据库中,认...
    99+
    2024-04-02
  • 简单谈谈Vue中的diff算法
    目录概述 虚拟Dom(virtual dom) 原理 实现过程 patch方法 sameVnode函数 patchVnode函数 updateChildren函数 结语 概述 di...
    99+
    2024-04-02
  • python简单实现整数反转的画解算法
    题目描述 给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。 如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 −...
    99+
    2024-04-02
  • 详解C/C++高精度算法的简单实现
    目录前言一、基本原理二、辅助方法1、字符串转高精度2、整型转高精度3、比较4、打印三、算法实现1、加法2、减法3、乘法4、除法四、使用示例1、加法2、减法3、乘法4、除法总结前言 由...
    99+
    2022-12-15
    C++实现高精度算法 C++高精度算法 C语言 高精度算法
  • 一文详解Vue3中简单diff算法的实现
    目录简单Diff算法减少DOM操作例子结论实现DOM复用与key的作用例子虚拟节点的key实现找到需要移动的元素探索节点顺序关系实现如何移动元素例子实现添加新元素例子实现移除不存在的...
    99+
    2024-04-02
  • python排序算法的简单实现方法
    1 冒泡排序  1.1 算法步骤: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元...
    99+
    2024-04-02
  • 排序算法图解之Java快速排序的分步刨析
    目录1.快速排序简介2.思路简介及图解3.实现代码及运行结果1.快速排序简介 快速排序是对冒泡排序的一种改进。基本思想为:通过一趟排序将要排序的数据分割为独立的两个部分,其中一部分的...
    99+
    2022-11-13
    Java快速排序算法 Java快速排序 Java 排序
  • 怎么手写最简单的LRU算法
    本篇内容主要讲解“怎么手写最简单的LRU算法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么手写最简单的LRU算法”吧!1 什么是LRULRU(Least r...
    99+
    2024-04-02
  • 如何利用javascript做简单的算法
    目录1 问题2 方法3 实验结果与讨论 1 问题 众所周知,无论是Pycharm或是IDLE、java都可以计算简单的算法,比如加减乘除。然而在Hbuilder中,javascrip...
    99+
    2024-04-02
  • 图解Java排序算法之3种简单排序
    目录简单选择排序代码实现冒泡排序代码实现直接插入排序代码实现总结排序是数据处理中十分常见且核心的操作,虽说实际项目开发中很小几率会需要我们手动实现,毕竟每种语言的类库中都有n多种关于...
    99+
    2024-04-02
  • android实现简单的乘法计算代码
    开发环境:android4.1.实验功能:在第一个界面中的2个乘数输入处分别输入2个数字,按下结果button,会自动跳到第二个界面并显示输入2个数字相乘的结果。如果在第一个界...
    99+
    2022-06-06
    Android
  • js如何实现日历的简单算法
    这篇文章将为大家详细讲解有关js如何实现日历的简单算法,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。最近有用到日历可需要编辑文本的日历,为了...
    99+
    2024-04-02
  • Android实现简单加法计算器
    本文实例为大家分享了Android实现简单加法计算器的具体代码,供大家参考,具体内容如下 package com.example.calculator; import an...
    99+
    2022-06-06
    Android
  • python tkinter 做个简单的计算器的方法
    背景 最近本菜鸡在学习 python GUI,从 tkinter 入门,想先做个小软件练习一下 思来想去,决定做一个 计算器 设计思路 首先,导入我们需要的包 — tkinte...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作