96SEO 2026-06-05 21:24 5
在啃 Vue3 源码的时候,翻到 patchChildren 这一块直接卡住了。网上搜了一圈,要么上来就丢一堆概念,要么就是贴一整段源码说"自己kan"。折腾了一段时间,终于把这块逻辑从头到尾捋顺了索性写篇文章记录一下也给正在啃源码的朋友搭把手。
说实话,Diff 算法听起来挺唬人,但拆开来kan其实就是一件事——页面geng新的时候,怎么用Zui小的代价把旧页面变成新页面。而 patchChildren 就是干这件事的核心函数。

这篇文章我会从Zui简单的版本开始,一步一步往上加功Neng,每一步douNeng跑通、Neng理解。跟着kan完,你对 Vue 的子节点geng新逻辑基本就Neng了然于胸了。
在讲代码之前,先说个前提。
Vue geng新页面的时候,不会直接操作真实 DOM。它会维护一份"虚拟 DOM",然后对比新旧虚拟 DOM 的差异,Zui后只把有变化的部分geng新到真实 DOM 上。
patchChildren 就是负责geng新某个父元素下面所有子节点的函数。它接收三个参数:
一句话概括它的职责:对比新旧子节点,该geng新的geng新,该新增的新增,该删的删。
我们先kan一个Zui基础的版本,只考虑"新旧子节点数量一样"的情况:
function patchChildren {
// 新子节点是纯文本
if {
// 文本geng新逻辑,先不管
}
// 新子节点是数组
else if ) {
const oldChildren = n1.children
const newChildren = n2.children
// 按下标一一对比,逐个geng新
for {
patch
}
}
else {
// 新无子节点,清空逻辑,先不管
}
}
逻辑hen简单粗暴——旧节点有几个,新节点就有几个,按下标顺序挨个调用 patch geng新。
patch 是 Vue 里负责单个节点geng新的函数:标签一样就改内容,标签不一样就销毁旧的创建新的。
这个版本Neng跑,但问题也hen明显:Ru果新旧子节点数量不一样呢? 多出来的怎么办?少了的怎么办?
接下来我们把逻辑补全,处理子节点数量不一致的情况:
function patchChildren {
if {
// 省略文本处理
}
else if ) {
const oldChildren = n1.children
const newChildren = n2.children
const oldLen = oldChildren.length
const newLen = newChildren.length
// 取较短的长度,算出Neng一一对应的部分
const commonLength = Math.min
// 第一步:Neng对上的,原地geng新
for {
patch
}
// 第二步:新节点geng多 → 多出来的要挂载
if {
for {
patch
}
}
// 第三步:旧节点geng多 → 多出来的要卸载
else if {
for {
unmount
}
}
}
else {
// 省略
}
}
拆开来kan这三步:
第一步,先把Neng一一对应的子节点geng新了。比如旧的有 3 个,新的有 5 个,那前 3 个先挨个geng新。
第二步,新的比旧的多,多出来的那些调用 patch。第一个参数传 null 意味着"没有旧节点",所以会直接创建新的真实 DOM 挂载到页面上。
第三步,旧的多新的少,多出来的旧节点调用 unmount 直接从页面删掉。
举个具体例子感受一下:
原来页面有 div1、div2,geng新后要变成 div1、div2、div3、div4。
反过来:
原来页面有 div1、div2、div3,geng新后只要 div1。
到这一步,基本的增删改douNeng处理了。但还有一个大问题——它只按下标顺序比对。Ru果子节点只是换了顺序,它不会聪明地移动 DOM,而是全部删掉重建,性Nenghen差。
这就是为什么 Vue 需要引入 key。
用过 Vue 的dou知道写 v-for 要加 :key,但hen多人可Neng不太清楚它底层到底干了什么。kan这段代码就明白了:
function patchChildren {
if {
// 省略
}
else if ) {
const oldChildren = n1.children
const newChildren = n2.children
// 遍历每一个新子节点
for {
const newVNode = newChildren
// 拿着新节点去旧节点里找 key 一样的
for {
const oldVNode = oldChildren
if {
// key 相同 → 是同一个元素,复用旧 DOM,只geng新内容
patch
break // 找到了就别找了处理下一个
}
}
}
}
}
key 就是每个节点的"身份证号"。身份证一样,就说明是同一个元素,只是内容变了不需要删掉重建,直接在原来的 DOM 上改就行。
打个比方:
旧页面有 3 个人:甲、乙、丙新页面要变成:乙、甲、丁
执行过程:
你kan,甲和乙只是换了顺序,但因为 key Neng对上,DOM 直接复用,不用销毁重建。这就是 key 的核心价值。
不过这个版本还有个问题——它Neng复用 DOM,但不会移动 DOM 的位置。也就是说虽然旧乙的 DOM 被复用了但它在页面上的物理位置没变,视觉上顺序还是错的。
所以我们需要进一步优化。
这版加了一个关键变量 lastIndex,用来记录上一个被复用的节点在旧数组里的位置。通过比较当前位置和上次位置,就Neng判断出元素是不是"往前挪了":
function patchChildren {
if {
// 省略
}
else if ) {
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0 // 记录旧节点中Zui大下标
for {
const newVNode = newChildren
for {
const oldVNode = oldChildren
if {
patch
if {
// 当前旧下标 <上次Zui大下标
// 说明这个元素往前挪了需要移动 DOM
} else {
// 顺序正常,不用移动,geng新Zui大下标
lastIndex = j
}
break
}
}
}
}
}
这个 j
旧 key 顺序:1,2,3新 key 顺序:2,1,4
嗯,你可Neng会问:判断出需要移动之后具体怎么移?这就是下一版要解决的问题。
光知道"要移动"还不够,还得知道"移到哪"。这一版引入了锚点 的概念:
function patchChildren {
if {
// 省略
}
else if ) {
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for {
const newVNode = newChildren
let find = false // 标记是否找到可复用的旧节点
for {
const oldVNode = oldChildren
if {
find = true
patch
if {
// 需要移动:找到新顺序里的前一个兄弟节点
const prevVNode = newChildren
if {
// 锚点 = 前一个节点的下一个兄弟元素
const anchor = prevVNode.el.nextSibling
// 把当前 DOM 插到锚点前面 = 放到前一个节点的后面
insert
}
} else {
lastIndex = j
}
break
}
}
// find 为 false:旧节点里没找到 → 这是新增节点
if {
const prevVNode = newChildren
let anchor = null
if {
// 有前兄弟节点,插到它后面
anchor = prevVNode.el.nextSibling
} else {
// 没有前兄弟,说明是第一个子元素,插到Zui前面
anchor = container.firstChild
}
// 创建新 DOM 并挂载到锚点位置
patch
}
}
}
}
这里有两块新逻辑,我分开说。
当判断出 j
说白了就是:我要站到前一个兄弟的后面。通过"前一个兄弟的下一个元素"作为锚点,就Neng精确定位。
注意这里多了一个 find 变量。内层循环跑完Ru果 find 还是 false,说明这个新节点在旧节点里完全找不到同 key 的,那就是个全新元素。
新增的时候同样需要锚点来决定插在哪:
patch 里第一个参数传 null,代表没有旧节点,走的是挂载逻辑,会创建新的真实 DOM。
顺便说一下patch 函数本身也Zuo了对应改造来支持锚点:
function patch {
if {
if {
// 全新节点,挂载时带上锚点
mountElement
} else {
// 有旧节点,走geng新逻辑
patchElement
}
}
// ...其他类型省略
}
mountElement 内部调用 insert,不传锚点就默认追加到Zui后传了就插到锚点前面。
前面处理了复用、移动、新增,还差一个:旧的节点里有些在新列表里Yi经不存在了需要删掉。
function patchChildren {
if {
// 省略
}
else if ) {
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
// ...前面复用、移动、新增的逻辑
for {
// ...
}
// ========== 新增:遍历旧节点,清理不需要的 ==========
for {
const oldVNode = oldChildren
// 拿旧节点的 key 去新列表里找
const has = newChildren.find
if {
// 新列表里找不到这个 key → 这个旧节点不需要了删掉
unmount
}
}
}
}
逻辑hen直白:遍历所有旧节点,拿着它的 key 去新列表里找,找不到就说明新页面Yi经不需要它了直接 unmount 删掉。
再举个完整的例子把所有逻辑串起来:
旧 key:1,2,3新 key:2,1,4
Zui终结果:key=3 被清理,key=4 被新增,key=1 和 key=2 被复用并移动到正确位置。整个geng新过程没有多余的 DOM 创建和销毁。
到这里patchChildren 的核心逻辑就完整了。我用一张流程图帮你把所有分支串起来:
patchChildren 被调用
│
├─ 新子节点是文本 → 走文本geng新逻辑
│
├─ 新子节点是数组 → 进入核心 Diff
│ │
│ ├─ 遍历新节点,用 key 去旧节点里匹配
│ │
│ ├─ 找到了
│ │ ├─ 复用旧 DOM,patch geng新内容
│ │ ├─ j = lastIndex → 不移动,geng新 lastIndex
│ │
│ └─ 没找到→ 新增节点,锚点精准插入
│
└─ 新无子节点 → 清空容器
成一句话:Neng复用就复用,该移动就移动,多了就新增,少了就删除。
这就是 Vue 简易版 Diff 子节点geng新的全部核心逻辑。当然Vue3 实际源码里用的是geng高效的快速 Diff 算法,但核心思想是一脉相承的。搞懂了这个简易版,再kan源码里的完整实现会轻松hen多。
啃源码这件事,说实话一开始挺痛苦的,尤其是 Diff 这块,变量多、嵌套深,hen容易kan着kan着就迷失了。但Ru果你Neng像我这样,从Zui简单的版本开始,一步一步往上加功Neng,每一步dou搞清楚"为什么要这样写",其实也没那么难。
希望这篇文章Neng帮到正在啃 Vue 源码的你。Ru果觉得有帮助,欢迎点赞收藏,有问题也欢迎在评论区交流。
参考:Vue.js 设计与实现 —— 霍春阳
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback