Vue3 源码解读系列(四)——组件更新

news2025/3/10 21:35:07

组件更新

组件更新流程:

  1. 从头部开始同步

  2. 从尾部开始同步

  3. 挂载剩余的新节点

  4. 删除多余的旧节点

  5. 处理未知的子序列

    • 当两个节点类型相同时,执行更新操作
    • 当新子节点中没有旧子节点中的某些节点时,执行删除操作
    • 当新子节点中多了旧子节点中没有的节点时,执行添加操作

    相对来说,这些操作中最麻烦的就是移动,既要判断哪些节点需要移动,也要清除如何移动。

    移动子节点(如何以最小的时间复杂度移动子节点才是重点

    • 在新旧子节点序列中找出相同节点并更新

      找出多余的节点删除,找出新的节点添加

      找出需要移动的节点,需要遍历对应的序列,如果在遍历旧子序列的过程中需要判断某个节点是否在新子序列中存在,这就需要双重循环,双重循环的复杂度是 O(n2),为了优化这个复杂度,建立索引图,把时间复杂度降低到 O(n)。

    • 建立索引图(空间换时间)

      在开发过程中,会给 v-for 生成的列表中的每一项分配唯一 key 作为项的唯一 ID。

      对比新旧子序列中的节点,key 相同的就是同一个节点,执行执行 patch 更新即可。

    • 移动和挂载新节点

      Vue3 是通过获取最长递增子序列来进行移动的,动态规划解法的时间复杂度为 O(n2),而 Vue3 采用了 ”贪心+二分查找“ 来实现,贪心的时间复杂度为 O(n),二分查找的时间复杂度 O(logn),总时间复杂度为 O(nlogn)。

在这里插入图片描述

/**
 * 比较节点
 */
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let i = 0 // 新、旧子序列的头部索引
  const l2 = c2.length
  let e1 = c1.length - 1 // 旧子节点的尾部索引
  let e2 = l2 - 1 // 新子节点的尾部索引

  // 1、从头部开始同步
  while (i <= e1 && i <= e2) {
    const n1 = c1[i] // 旧节点
    const n2 = c2[i] // 新节点
    // 相同类型的节点,递归执行 patch 更新节点
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
    }
    // 节点类型不同 或 不存在新 | 旧节点,则退出
    else {
      break
    }
    i++
  }

  // 2、从尾部开始同步
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1] // 旧节点
    const n2 = c2[e2] // 新节点
    // 相同类型的节点,递归执行 patch 更新节点
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
    }
    // 节点类型不同 或 不存在新 | 旧节点,则退出
    else {
      break
    }
    e1--
    e2--
  }

  // 3、挂载剩余的新节点
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor
      while (i <= e2) {
        // 挂载新节点
        patch(null, c2[i], container, anchor, parentComponent, parentSuspense, isSVG)
        i++
      }
    }
  }

  // 4、删除多余的旧节点
  else if (i > e2) {
    while (i <= e1) {
      // 删除节点
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }

  // 5、处理未知的子序列
  // 5.1、根据 key 建立新子序列的索引图
  const s1 = i // 旧子序列开始索引,从 i 开始记录
  const s2 = i // 新子序列开始索引,从 i 开始记录
  const keyToNewIndexMap = new Map() // 新子序列节点的 key -> index 的索引表
  // 遍历新子序列,记录索引表
  for (i = s2; i <= e2; i++) {
    const nextChild = c2[i]
    keyToNewIndexMap.set(nextChild.key, i)
  }

  // 5.2、正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点
  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
  }
  // 正序遍历旧子序列
  for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]
    // 所有新的子序列节点都已经更新,删除剩余的节点
    if (patched >= toBePatched) {
      unmount(prevChild, parentComponent, parentSuspense, true)
      continue
    }
    let newIndex = keyToNewIndexMap.get(prevChild.key) // 查找旧子序列中的节点在新子序列中的索引
    // 找不到说明旧子序列已经不存在于新子序列中,则删除该节点
    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], container, null, parentComponent, parentSuspense, isSVG, optimized)
      patched++
    }
  }

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

/**
 * 获取最长递增子序列,实际求的是最长递增子序列各项的索引
 */
function getSequence(arr) {
  const p = arr.slice()
  const result = [0] // 长度为 i 的最长递增子序列各项的索引
  let i, j, u, v, c
  const len = arr.length
  // 对数组遍历,依次求解长度为 i 时的最长递增子序列
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      // 当 i 元素大于 i-1 的元素时,添加 i 元素并更新最长子序列
      if (arr[j] < arrI) {
        // 存储在 result 更新前的最后一个索引的值
        p[i] = j
        result.push(i)
        continue
      }
      // 否则往前查找直到找到一个比 i 小的元素,然后插在该元素后面并更新对应的最长递增子序列
      u = 0
      v = result.length - 1
      // 二分搜索,查找比 arrI 小的节点,更新 result 的值
      while (u < v) {
        c = ((u + v) / 2) | 0
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]

  // 回溯数组 p,找到最终的索引
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

问题:v-for 时能否用 index 作为 key?

答:如果列表只是用于展示的化没有问题,如果列表涉及增、删、改就一定要用唯一标识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1198384.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小样本目标检测(Few-Shot Object Detection)综述

背景 前言:我的未来研究方向就是这个,所以会更新一系列的文章,就关于FSOD,如果有相同研究方向的同学欢迎沟通交流,我目前研一,希望能在研一发文,目前也有一些想法,但是具体能不能实现还要在做的过程中慢慢评估和实现.写文的主要目的还是记录,避免重复劳动,我想用尽量简洁的语言…

141.环形链表(LeetCode)

想法一 快慢指针&#xff0c;设置slow和fast指针&#xff0c;slow一次走一步&#xff0c;fast一次走两步&#xff0c;如果链表有环&#xff0c;它们最终会相遇&#xff0c;相遇时返回true&#xff1b;如果链表无环&#xff0c;它们最终走到空&#xff0c;跳出循环&#xff0c;…

计算机视觉中目标检测的数据预处理

本文涵盖了在解决计算机视觉中的目标检测问题时&#xff0c;对图像数据执行的预处理步骤。 首先&#xff0c;让我们从计算机视觉中为目标检测选择正确的数据开始。在选择计算机视觉中的目标检测最佳图像时&#xff0c;您需要选择那些在训练强大且准确的模型方面提供最大价值的图…

自动化测试 —— requests和selenium模块!

一、requests基于POST请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #1.requests的GET与POST用法的区别&#xff1a; GET请求: (HTTP默认的请求方法就是GET) * 没有请求体 * 数据必须在1K之内&#xff01; * GET请求数据会暴露在浏览器…

YOLOv5算法进阶改进(1)— 改进数据增强方式 + 添加CBAM注意力机制

前言:Hello大家好,我是小哥谈。本节课设计了一种基于改进YOLOv5的目标检测算法。首先在数据增强方面使用Mosaic-9方法来对训练集进行数据增强,使得网络具有更好的泛化能力,从而更好适用于应用场景。而后,为了更进一步提升检测精度,在backbone中嵌入了CBAM注意力机制模块,…

uniapp中在组件中使用被遮挡或层级显示问题

uniapp中在组件中使用或croll-view标签内使用uni-popup在真机环境下会被scroll-view兄弟元素遮挡&#xff0c;在开发环境下和安卓系统中可以正常显示&#xff0c;但在ios中出现了问题 看了许多文章都没有找到问题的原因&#xff0c;最后看到这一个文章http://t.csdnimg.cn/pvQ…

21.合并两个有序链表(LeetCode)

合并两个有序链表&#xff0c;是链表的经典题之一 &#xff0c;这里给出一种经典解法 想法一 创建head和tail两个指针&#xff0c;从头比较两个链表&#xff0c;取小的尾插&#xff0c;注意一开始指针的初始化&#xff0c;接着就是不断利用tail指针&#xff0c;链接比较之中较…

C语言----静态链接库和动态链接库

在前面的文章中讲到可执行程序的生成需要经过预处理&#xff0c;编译&#xff0c;汇编和链接四个步骤&#xff0c;链接阶段是链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。 需要解读一下库文件&#xff0c;我们可以将库文件等价为压缩包文件&…

AIGC ChatGPT 4 轻松实现小游戏开发制作

贪吃蛇的小游戏相信大家都玩儿过,我们让ChatGPT4来帮我们制作一个贪吃蛇的小游戏。 在ChatGPT中发送Prompt如下图: 完整代码如下: <!DOCTYPE html> <html> <head> <title>贪吃蛇游戏</title> <style type="text/css"> #can…

电脑小Tip---外接键盘F1-F12快捷键与笔记本不同步

当笔记本外接一款非常好用的静音键盘后&#xff0c;会出现一些问题。例如&#xff1a;外接键盘F1-F12与笔记本不同步。具体一个例子就是&#xff0c;在运行matlab程序时&#xff0c;需要点编辑器—运行&#xff0c;这样就很麻烦&#xff0c;直接运行的快捷键是笔记本键盘上的F5…

推荐 8 款OCR工具(二)完结篇

双十一&#xff0c;又要剁手了&#xff0c;但我还是 推荐 8 款OCR工具&#xff01; 当你感到迷茫时&#xff0c;不妨停下来&#xff0c;深呼吸&#xff0c;重新审视自己所处的位置和你的内心。这样的简单行为可能会帮助你找到方向。 SimpleOCR 网址&#xff1a;https://simple…

时间序列预测实战(九)PyTorch实现LSTM-ARIMA融合移动平均进行长期预测

一、本文介绍 本文带来的是利用传统时间序列预测模型ARIMA(注意&#xff1a;ARIMA模型不属于机器学习)和利用PyTorch实现深度学习模型LSTM进行融合进行预测&#xff0c;主要思想是->先利用ARIMA先和移动平均结合处理数据的线性部分&#xff08;例如趋势和季节性&#xff09…

删除成绩(数组)

任务要求 设计程序&#xff0c;实现从多名学生某门课程的成绩查找到第一个不及格的成绩&#xff0c;删除其成绩&#xff0c;输出删除成绩后的多名学生这一门课程的成绩。任务保证至少存在1个学生的成绩为不及格。

短信验证码实现(阿里云)

如果实现短信验证&#xff0c;上教程&#xff0c;这里用的阿里云短信服务 短信服务 (aliyun.com) 进入短信服务后开通就行&#xff0c;可以体验100条免费&#xff0c;刚好测试用 这里由自定义和专用&#xff0c;测试的话就选择专用吧&#xff0c;自定义要审核&#xff0c; Se…

Linux-系统调优-常见命令

目录 1、uptime 2、/proc/loadavg文件&#xff1a;获取平均负载的信息 3、free 命令&#xff1a;查看内存使用的详细情况 基础信息 buffer/cache介绍 4、SWAP 交换分区 基础信息 如何定义使用SWAP 交换分区 5、vmstat&#xff1a;性能监控工具 基础信息 性能影响&am…

回调地狱 与 Promise(JavaScript)

目录捏 前言一、异步编程二、回调函数三、回调地狱四、Promise1. Promise 简介2. Promise 语法3. Promise 链式 五、总结 前言 想要学习Promise&#xff0c;我们首先要了解异步编程、回调函数、回调地狱三方面知识&#xff1a; 一、异步编程 异步编程技术使你的程序可以在执行一…

帧同步的思想与FIFO复位

02基于FDMA三缓存构架_哔哩哔哩_bilibili 图像从外部传输进来的时候&#xff0c;会产生若干延迟&#xff0c;可能会出现各种各样的问题&#xff08;断帧等&#xff09;&#xff0c;此时可以通过VS信号清空FIFO进行复位。 这个过程中的复位信号可能需要拓展&#xff0c;这是因为…

mysql 讲解(1)

文章目录 前言一、基本的命令行操作二、操作数据库语句2.1、创建数据库2.2、删除数据库2.3、使用数据库2.4 查看所有数据库 三、列的数据类型3.1 字符串3.2 数值3.3 时间日期3.4 空3.5 int 和 varchar问题总结&#xff1a; 四、字段属性4.1 UnSigned4.2 ZEROFILL4.3 Auto_InCre…

【python海洋专题四十六】研究区域示意放大图

【python海洋专题四十六】研究区域示意放大图 图片 往期推荐 图片 【python海洋专题一】查看数据nc文件的属性并输出属性到txt文件 【python海洋专题二】读取水深nc文件并水深地形图 【python海洋专题三】图像修饰之画布和坐标轴 【Python海洋专题四】之水深地图图像修饰 …

Vuex:模块化Module :VOA模式

由于使用单一状态树&#xff0c;应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时&#xff0c;store 对象就有可能变得相当臃肿。 这句话的意思是&#xff0c;如果把所有的状态都放在/src/store/index.js中&#xff0c;当项目变得越来越大的时候&#xff0c;Vue…