8.9 更新子节点
更新子节点是对一个元素打补丁的最后一步;为了让更新逻辑写得下去,先把子节点类型规范化,再按新旧类型组合分情况处理。
一、规范化 children:只有三种形态
| 形态 | vnode.children 的值 | 例子 |
|---|---|---|
| 没有子节点 | null | { type: 'div', children: null } |
| 文本子节点 | 字符串 | { type: 'div', children: 'Some Text' } |
| 其他情况 | 数组 | { type: 'div', children: [{ type: 'p' }, 'Some Text'] } |
单个元素子节点、多个子节点、文本与元素混合,一律用数组表示。
只有子节点的类型是规范化的,才有利于编写更新逻辑 —— 任意节点都落到这三者之一,才能穷举组合。
二、九种组合及其处理
新旧子节点各三种形态,组合出九种情况:
| n1.children \ n2.children | null | 字符串 | 数组 |
|---|---|---|---|
null | 什么都不做 | 设置文本 | 逐个挂载 |
| 字符串 | 清空文本 | 更新文本 | 清空文本 + 逐个挂载 |
| 数组 | 逐个卸载 | 逐个卸载 + 设置文本 | 逐个卸载 + 逐个挂载(暂用「傻瓜式」,真正的 Diff 见第 9–11 章) |
落实到代码,其实不需要完全覆盖九种可能,只需按 n2.children 的三种形态分支:
function patchChildren(n1, n2, container) {
if (typeof n2.children === 'string') {
// 新子节点是文本节点
// 旧子节点三种可能:不存在、文本、一组子节点
// 只有旧是一组子节点时才需要逐个卸载,其他情况什么都不做
if (Array.isArray(n1.children)) {
n1.children.forEach(c => unmount(c))
}
// 最后将新的文本内容设置给容器元素
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
// 新子节点是一组子节点
if (Array.isArray(n1.children)) {
// 新旧都是一组子节点 → 涉及核心的 Diff 算法
// 暂时全部卸载再全部挂载
n1.children.forEach(c => unmount(c))
n2.children.forEach(c => patch(null, c, container))
} else {
// 旧子节点是文本节点或不存在 → 清空容器后逐个挂载新的一组子节点
setElementText(container, '')
n2.children.forEach(c => patch(null, c, container))
}
} else {
// 新子节点不存在
if (Array.isArray(n1.children)) {
// 旧子节点是一组子节点 → 逐个卸载
n1.children.forEach(c => unmount(c))
} else if (typeof n1.children === 'string') {
// 旧子节点是文本节点 → 清空内容
setElementText(container, '')
}
// 旧子节点也不存在 → 什么都不需要做
}
}
「把旧的一组子节点全部卸载、再把新的一组子节点全部挂载」是功能可用但性能差的权宜之计:完全没有复用 DOM,第 9–11 章的 Diff 算法就是来替换这个分支的。
三、patchElement:更新阶段的两步
patch 在新旧类型相同且 n1 存在时调用 patchElement:先更新 props,再更新 children。
function patchElement(n1, n2) {
const el = n2.el = n1.el // 复用同一个 DOM 元素
const oldProps = n1.props
const newProps = n2.props
// 第一步:更新 props
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
// 新 props 中已不存在 → 传 null,让 patchProps 卸载该属性 / 解绑事件
patchProps(el, key, oldProps[key], null)
}
}
// 第二步:更新 children
patchChildren(n1, n2, el)
}
两个循环缺一不可:遍历
newProps负责设置 / 更新,遍历oldProps负责删除(事件属性传null会解绑,见 8.7)。
相关
- 8.7 事件的处理:
patchProps(el, key, prevValue, null)为什么能解绑事件 - 8.10 文本节点和注释节点:children 里还会出现文本 / 注释类型的 vnode