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

组件更新

组件更新流程:

  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?

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android——Gradle插件gradle-wrapper.properties

一、Android Studio版本&#xff0c;Android Gradle插件版本&#xff0c;Gradle版本 Android Studio 通过Android Gradle插件 使用 Gradle来构建代码&#xff1b; Android Studio每次升级后&#xff0c; Android Gradle 插件自动更新&#xff0c;对应的Gradle版本也会变动&…

【MybatisPlus】条件构造器、自定义SQL、Service接口

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 MybatisPlus 一、条件构造器1.1 基于QueryW…

MemcachedRedis构建缓存服务器 (数据持久化,主从同步,哨兵模式)

Memcached/redis是高性能的分布式内存缓存服务器,通过缓存数据库查询结果&#xff0c;减少数据库访问次数&#xff0c;以提高动态Web等应用的速度、 提高可扩展性。降低数据库读的压力 Nsql的优点&#xff1a;高可扩展性&#xff0c;分布式计算&#xff0c;低成本&#xff0c;…

AI绘画神器DALLE 3的解码器:一步生成的扩散模型之Consistency Models

前言 关于为何写此文&#xff0c;说来同样话长啊&#xff0c;历程如下 我司LLM项目团队于23年11月份在给一些B端客户做文生图的应用时&#xff0c;对比了各种同类工具&#xff0c;发现DALLE 3确实强&#xff0c;加之也要在论文100课上讲DALLE三代的三篇论文&#xff0c;故此文…

javax.management.InstanceNotFoundException: Catalina:type=Server错误的解决

软件&#xff1a; JDK 1.8 Tomcat 8.5.66 IDEA 2019.3.3 问题&#xff1a;启动IDEA新建一Web Application项目&#xff0c;设置好项目运行&#xff0c;结果发现提示&#xff1a; 提示&#xff1a;Application Server was not connected before run configuration stop, rea…

【AI】生成模型变得简单:了解它们的工作原理和不同类型

什么是生成模型&#xff1f; 在不断发展的人工智能领域&#xff0c;生成模型已成为人工智能技术最具吸引力和创造力的方面之一。这些模型是创意人工智能的核心&#xff0c;它们有能力生成各种内容&#xff0c;从栩栩如生的图像和引人入胜的文本到令人着迷的音乐和创新的艺术作…

linux入门---线程池的模拟实现

目录标题 什么是线程池线程的封装准备工作构造函数和析构函数start函数join函数threadname函数完整代码 线程池的实现准备工作构造函数和析构函数push函数pop函数run函数完整的代码 测试代码 什么是线程池 在实现线程池之前我们先了解一下什么是线程池&#xff0c;所谓的池大家…

【postgresql】CentOS7 安装pgAdmin 4

CentOS7 安装PostgreSQL Web管理工具pgAdmin 4。 pgAdmin 是世界上最先进的开源数据库 PostgreSQL 最受欢迎且功能丰富的开源管理和开发平台。 下载地址&#xff1a; pgadmin-4 download pgAdmin 4分为桌面版和服务器版。 我们这里部署服务器版本。 安装RPM包。 安装源 s…

【数据结构】树与二叉树(十一):二叉树的层次遍历(算法LevelOrder)

文章目录 5.2.1 二叉树二叉树性质引理5.1&#xff1a;二叉树中层数为i的结点至多有 2 i 2^i 2i个&#xff0c;其中 i ≥ 0 i \geq 0 i≥0。引理5.2&#xff1a;高度为k的二叉树中至多有 2 k 1 − 1 2^{k1}-1 2k1−1个结点&#xff0c;其中 k ≥ 0 k \geq 0 k≥0。引理5.3&…

最长有效括号

给你一个只包含 ‘(’ 和 ‘)’ 的字符串&#xff0c;找出最长有效&#xff08;格式正确且连续&#xff09;括号子串的长度。 class Solution {public int longestValidParentheses(String s) {Stack<Integer> st new Stack<Integer>();int ans 0;for(int i 0…

Linux C 时间编程

时间编程 Linux中时间相关命令时间编程time  获取当前的时间gmtime  获取当前日期时间localtime  获取本地时间日期asctime  规格时间结构体为字符串 Linux中时间相关命令 1&#xff09;date&#xff1a;打印当前的系统时间。 2&#xff09;date -s 20231111&#xff…

封神教程:腾讯云3年轻量应用服务器老用户购买方法

腾讯云轻量应用服务器特价是有新用户限制的&#xff0c;所以阿腾云建议大家选择3年期轻量应用服务器&#xff0c;一劳永逸&#xff0c;免去续费困扰。腾讯云轻量应用服务器3年优惠可以选择2核2G4M和2核4G5M带宽&#xff0c;3年轻量2核2G4M服务器540元&#xff0c;2核4G5M轻量应…

Linux 进程优先级 | 环境变量

目录 进程优先级 基本概念 认识优先级 PRI and NI NI值的范围 查看进程优先级 用top命令更改已存在进程的nice&#xff1a; 如何修改优先级 其他概念 环境变量 基本概念 常见环境变量 和环境变量相关的命令 环境变量的组织方式 通过代码如何获取环境变量 环境变量通…

arcgis基础篇--实验

一、绘制带空洞的面要素 方法一&#xff1a;先绘制出一个面区域&#xff0c;然后在面上再绘制一个面区域代表面洞&#xff0c;两者位于同一个图层内&#xff0c;选中代表面洞的区域&#xff0c;选择【编辑器】-【裁剪】工具&#xff0c;将面裁剪出一个洞&#xff0c;随后删除代…

SpringBoot--中间件技术-2:整合redis,redis实战小案例,springboot cache,cache简化redis的实现,含代码

SpringBoot整合Redis 实现步骤 导pom文件坐标 <!--redis依赖--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>yaml主配置文件&#xff0c;配置…

基于python+TensorFlow+Django卷积网络算法+深度学习模型+蔬菜识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 介绍了TensorFlow在图像识别分类中的应用&#xff0c;并通过相关代码进行了讲解。通过TensorFlow提供的工具和库&am…

K8S知识点(八)

&#xff08;1&#xff09;实战入门-Label 通过标签实现Pod的区分&#xff0c;说白了就是一种标签选择机制 可以使用命令是否加了标签&#xff1a; 打标签&#xff1a; 更新标签&#xff1a; 筛选标签&#xff1a; 修改配置文件&#xff0c;重新创建一个pod 筛选&#xff1…

Python---split()方法 + join()方法

split()方法 split 英 /splɪt/ v. 分裂&#xff0c;使分裂&#xff08;成不同的派别&#xff09;&#xff1b;分开&#xff0c;使分开&#xff08;成为几个部份&#xff09;&#xff1b;&#xff08;使&#xff09;撕裂&#xff1b;分担&#xff0c;分享&#xff1b;划破&…

kubeadm部署k8s及高可用

目录 CNI 网络组件 1、flannel的功能 2、flannel的三种模式 3、flannel的UDP模式工作原理 4、flannel的VXLAN模式工作原理 5、Calico主要组成部分 6、calico的IPIP模式工作原理 7、calico的BGP模式工作原理 8、flannel 和 calico 的区别 Kubeadm部署k8s及高可用 1、…

如何用 GPTs 构建自己的知识分身?(进阶篇)

&#xff08;注&#xff1a;本文为小报童精选文章&#xff0c;已订阅小报童或加入知识星球「玉树芝兰」用户请勿重复付费&#xff09; 有了这些改进&#xff0c;你可以快速判断 GPT 助手给出的答案是真实还是「幻觉」了。 问题 在《如何用自然语言 5 分钟构建个人知识库应用&am…