Skip to content

Commit 4e5947e

Browse files
committed
✨feat: 完善 8.9更新子节点
1 parent e6e072c commit 4e5947e

File tree

2 files changed

+151
-83
lines changed

2 files changed

+151
-83
lines changed

第8章挂载与更新/8.9_更新子节点/8.9_更新子节点.js

+124-72
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,116 @@ function normalizeClass(classValue) {
4040
}
4141
function unmount(vnode) {
4242
var _a;
43+
if (typeof vnode === "string")
44+
return;
4345
const parent = (_a = vnode.el) === null || _a === void 0 ? void 0 : _a.parentNode;
4446
if (parent && vnode.el) {
4547
parent.removeChild(vnode.el);
4648
}
4749
}
50+
function patchProps(el, key, prevValue, nextValue) {
51+
// 匹配以 on 开头的属性,视其为事件
52+
if (/^on/.test(key)) {
53+
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
54+
const invokers = el._vei || (el._vei = {});
55+
//根据事件名称获取 invoker
56+
let invoker = invokers[key];
57+
const name = key.slice(2).toLowerCase();
58+
if (nextValue) {
59+
if (!invoker) {
60+
// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
61+
// vei 是 vue event invoker 的首字母缩写
62+
invoker = el._vei[key] = (e) => {
63+
// 如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数
64+
// 如果事件发生的时间早于事件处理函数绑定的时间,则不处理执行事件处理函数
65+
if (e.timeStamp < invoker.attached)
66+
return;
67+
if (Array.isArray(invoker.value)) {
68+
invoker.value.forEach((fn) => fn(e));
69+
}
70+
else {
71+
// 否则直接作为函数调用
72+
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
73+
invoker.value(e);
74+
}
75+
};
76+
// 将真正的事件处理函数赋值给 invoker.value
77+
invoker.value = nextValue;
78+
// 添加 invoker.attached 属性,存储事件处理函数被绑定的时间
79+
invoker.attached = performance.now();
80+
// 绑定 invoker 作为事件处理函数
81+
el.addEventListener(name, invoker);
82+
}
83+
else {
84+
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
85+
invoker.value = nextValue;
86+
}
87+
}
88+
else if (invoker) {
89+
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
90+
el.removeEventListener(name, invoker);
91+
}
92+
}
93+
else if (key === 'class') {
94+
el.className = nextValue || '';
95+
}
96+
else if (shouldSetAsProps(el, key, nextValue)) {
97+
const type = typeof el[key];
98+
if (type === 'boolean' && nextValue === '') {
99+
el[key] = true;
100+
}
101+
else {
102+
el[key] = nextValue;
103+
}
104+
}
105+
else {
106+
el.setAttribute(key, nextValue);
107+
}
108+
}
48109
function patchElement(n1, n2) {
110+
const el = n2.el = n1.el;
111+
const oldProps = n1.props;
112+
const newProps = n2.props;
113+
// 第一步:更新 props
114+
for (const key in newProps) {
115+
if (newProps[key] !== (oldProps === null || oldProps === void 0 ? void 0 : oldProps[key])) {
116+
el && patchProps(el, key, oldProps === null || oldProps === void 0 ? void 0 : oldProps[key], newProps[key]);
117+
}
118+
}
119+
for (const key in oldProps) {
120+
if (newProps && !(key in newProps)) {
121+
el && patchProps(el, key, oldProps[key], null);
122+
}
123+
}
124+
// 第二步:更新 children
125+
patchChildren(n1, n2, el);
126+
}
127+
function patchChildren(n1, n2, container) {
128+
// 判断新子节点的类型是否是文本节点
129+
if (typeof n2.children === 'string') {
130+
// 旧子节点的类型有三种可能:
131+
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
132+
if (Array.isArray(n1.children)) {
133+
// 这里涉及到diff算法
134+
n1.children.forEach((c) => unmount(c));
135+
}
136+
// 最后将新的文本节点内容设置给容器元素
137+
setElementText(container, n2.children);
138+
}
139+
else if (Array.isArray(n2.children)) {
140+
// 说明新子节点是一组子节点
141+
// 判断旧子节点是否也是一组子节点
142+
if (Array.isArray(n1.children)) {
143+
// 代码运行到这里,则说明新旧子节点都是一组子节点,这里涉及核心的 Diff 算法
144+
}
145+
else {
146+
// 此时:
147+
// 旧子节点要么是文本子节点,要么不存在
148+
// 但无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载
149+
setElementText(container, '');
150+
n2.children.forEach(c => patch(null, c, container));
151+
}
152+
}
49153
}
50154
function createRenderer(options) {
51155
// 通过 options 得到操作 DOM 的 API
@@ -73,6 +177,8 @@ function createRenderer(options) {
73177
insert(el, container);
74178
}
75179
function patch(n1, n2, container) {
180+
if (typeof n2 === 'string')
181+
return;
76182
// 如果 n1 存在,则对比 n1 和 n2 的类型
77183
if (n1 && n1.type !== n2.type) {
78184
// 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
@@ -125,66 +231,10 @@ const renderer = createRenderer({
125231
insert(el, parent, anchor) {
126232
parent.insertBefore(el, anchor);
127233
},
128-
// 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
129234
patchProps(el, key, prevValue, nextValue) {
130-
// 匹配以 on 开头的属性,视其为事件
131-
if (/^on/.test(key)) {
132-
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
133-
const invokers = el._vei || (el._vei = {});
134-
//根据事件名称获取 invoker
135-
let invoker = invokers[key];
136-
const name = key.slice(2).toLowerCase();
137-
if (nextValue) {
138-
if (!invoker) {
139-
// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
140-
// vei 是 vue event invoker 的首字母缩写
141-
invoker = el._vei[key] = (e) => {
142-
// 如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数
143-
// 如果事件发生的时间早于事件处理函数绑定的时间,则不处理执行事件处理函数
144-
if (e.timeStamp < invoker.attached)
145-
return;
146-
if (Array.isArray(invoker.value)) {
147-
invoker.value.forEach((fn) => fn(e));
148-
}
149-
else {
150-
// 否则直接作为函数调用
151-
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
152-
invoker.value(e);
153-
}
154-
};
155-
// 将真正的事件处理函数赋值给 invoker.value
156-
invoker.value = nextValue;
157-
// 添加 invoker.attached 属性,存储事件处理函数被绑定的时间
158-
invoker.attached = performance.now();
159-
// 绑定 invoker 作为事件处理函数
160-
el.addEventListener(name, invoker);
161-
}
162-
else {
163-
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
164-
invoker.value = nextValue;
165-
}
166-
}
167-
else if (invoker) {
168-
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
169-
el.removeEventListener(name, invoker);
170-
}
171-
}
172-
else if (key === 'class') {
173-
el.className = nextValue || '';
174-
}
175-
else if (shouldSetAsProps(el, key, nextValue)) {
176-
const type = typeof el[key];
177-
if (type === 'boolean' && nextValue === '') {
178-
el[key] = true;
179-
}
180-
else {
181-
el[key] = nextValue;
182-
}
183-
}
184-
else {
185-
el.setAttribute(key, nextValue);
186-
}
235+
patchProps(el, key, prevValue, nextValue);
187236
}
237+
// 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
188238
});
189239
// 不同事件名称的vnode
190240
// const vnode = {
@@ -200,21 +250,23 @@ const renderer = createRenderer({
200250
// children: 'text'
201251
// }
202252
// 相同事件名称不同函数的vnode
253+
// 没有子节点
254+
// vnode = {
255+
// type: 'div',
256+
// children: null
257+
// }
258+
// // 文本子节点
259+
// vnode = {
260+
// type: 'div',
261+
// children: 'Some Text'
262+
// }
263+
// // 其他情况,子节点使用数组表示
203264
const vnode = {
204-
type: 'p',
205-
props: {
206-
onClick: [
207-
// 第一个事件处理函数
208-
() => {
209-
alert('clicked 1');
210-
},
211-
// 第二个事件处理函数
212-
() => {
213-
alert('clicked 2');
214-
}
215-
]
216-
},
217-
children: 'text'
265+
type: 'div',
266+
children: [
267+
{ type: 'p' },
268+
'Some Text'
269+
]
218270
};
219271
// 初次挂载
220272
renderer.render(vnode, document.querySelector('#app'));

第8章挂载与更新/8.9_更新子节点/8.9_更新子节点.ts

+27-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
interface VNode {
22
type: string;
33
props?: { [key: string]: any }; // 属性对象可以包含任意键值对
4-
children?: string | VNode[];
4+
children?: string | (VNode | string)[];
55
el?: HTMLElement;
66
}
77

@@ -52,13 +52,14 @@ function normalizeClass(classValue: string | { [key: string]: boolean } | Array<
5252
// 将结果集合转换为数组,并用空格连接成字符串,并去除首尾空格后返回
5353
return Array.from(resultClassSet).join(' ').trim();
5454
}
55-
function unmount(vnode: VNode) {
55+
function unmount(vnode: VNode | string) {
56+
if (typeof vnode === "string") return
5657
const parent = vnode.el?.parentNode
5758
if (parent && vnode.el) {
5859
parent.removeChild(vnode.el)
5960
}
6061
}
61-
function patchProps(element: HTMLElement, key: string, prevValue: string | EventListenerOrEventListenerObject | null, nextValue: string | EventListenerOrEventListenerObject) {
62+
function patchProps(el: HTMLElement, key: string, prevValue: string | EventListenerOrEventListenerObject | null, nextValue: string | EventListenerOrEventListenerObject | null) {
6263
// 匹配以 on 开头的属性,视其为事件
6364
if (/^on/.test(key)) {
6465
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
@@ -110,35 +111,49 @@ function patchProps(element: HTMLElement, key: string, prevValue: string | Event
110111
}
111112
}
112113

113-
function patchElement(n1, n2) {
114+
function patchElement(n1: VNode, n2: VNode) {
114115
const el = n2.el = n1.el
115116
const oldProps = n1.props
116117
const newProps = n2.props
117118
// 第一步:更新 props
118119
for (const key in newProps) {
119-
if (newProps[key] !== oldProps[key]) {
120-
patchProps(el, key, oldProps[key], newProps[key])
120+
if (newProps[key] !== oldProps?.[key]) {
121+
el && patchProps(el, key, oldProps?.[key], newProps[key])
121122
}
122123
}
123124
for (const key in oldProps) {
124-
if (!(key in newProps)) {
125-
patchProps(el, key, oldProps[key], null)
125+
if (newProps && !(key in newProps)) {
126+
el && patchProps(el, key, oldProps[key], null)
126127
}
127128
}
128129

129130
// 第二步:更新 children
130131
patchChildren(n1, n2, el)
131132
}
132-
function patchChildren(n1, n2, container) {
133+
function patchChildren(n1: VNode, n2: VNode, container: HTMLElement | undefined) {
133134
// 判断新子节点的类型是否是文本节点
134135
if (typeof n2.children === 'string') {
135-
// 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
136+
// 旧子节点的类型有三种可能:
136137
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
137138
if (Array.isArray(n1.children)) {
139+
// 这里涉及到diff算法
138140
n1.children.forEach((c) => unmount(c))
139141
}
140142
// 最后将新的文本节点内容设置给容器元素
141143
setElementText(container, n2.children)
144+
} else if (Array.isArray(n2.children)) {
145+
// 说明新子节点是一组子节点
146+
147+
// 判断旧子节点是否也是一组子节点
148+
if (Array.isArray(n1.children)) {
149+
// 代码运行到这里,则说明新旧子节点都是一组子节点,这里涉及核心的 Diff 算法
150+
} else {
151+
// 此时:
152+
// 旧子节点要么是文本子节点,要么不存在
153+
// 但无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载
154+
setElementText(container, '')
155+
n2.children.forEach(c => patch(null, c, container))
156+
}
142157
}
143158
}
144159
function createRenderer(options: Options) {
@@ -170,7 +185,8 @@ function createRenderer(options: Options) {
170185
// 调用 insert 函数将元素插入到容器内
171186
insert(el, container)
172187
}
173-
function patch(n1: VNode | null, n2: VNode, container: HTMLElement) {
188+
function patch(n1: VNode | null, n2: VNode | string, container: HTMLElement) {
189+
if (typeof n2 === 'string') return
174190
// 如果 n1 存在,则对比 n1 和 n2 的类型
175191
if (n1 && n1.type !== n2.type) {
176192
// 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载

0 commit comments

Comments
 (0)