Skip to content

Commit 57a33d2

Browse files
committed
✨feat: 完善8.11Fragment
1 parent 73238d5 commit 57a33d2

File tree

4 files changed

+709
-0
lines changed

4 files changed

+709
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
"use strict";
2+
function shouldSetAsProps(el, key, value) {
3+
// 特殊处理
4+
if (key === 'form' && el.tagName === 'INPUT')
5+
return false;
6+
// 兜底
7+
return key in el;
8+
}
9+
function handleObject(set, obj) {
10+
for (const key in obj) {
11+
// 如果对象的值为 true,则将键(类名)加入到 set 中
12+
if (obj[key])
13+
set.add(key);
14+
}
15+
}
16+
function normalizeClass(classValue) {
17+
// 如果 classValue 是字符串,则直接返回
18+
if (typeof classValue === 'string')
19+
return classValue;
20+
// 创建一个 Set 来存储结果类名
21+
let resultClassSet = new Set();
22+
// 处理数组和对象的情况
23+
if (Array.isArray(classValue)) {
24+
// 遍历数组中的每个值
25+
for (const value of classValue) {
26+
// 如果值是字符串,则直接添加到结果集合中
27+
if (typeof value === 'string')
28+
resultClassSet.add(value);
29+
// 如果值是对象,则调用 handleObject 处理
30+
else
31+
handleObject(resultClassSet, value);
32+
}
33+
}
34+
else {
35+
// 如果 classValue 是对象,则调用 handleObject 处理
36+
handleObject(resultClassSet, classValue);
37+
}
38+
// 将结果集合转换为数组,并用空格连接成字符串,并去除首尾空格后返回
39+
return Array.from(resultClassSet).join(' ').trim();
40+
}
41+
function createRenderer(options) {
42+
// 通过 options 得到操作 DOM 的 API
43+
const { createElement, insert, createText, setText, setElementText, patchProps } = options;
44+
function unmount(vnode) {
45+
var _a;
46+
if (typeof vnode === "string")
47+
return;
48+
const parent = (_a = vnode.el) === null || _a === void 0 ? void 0 : _a.parentNode;
49+
if (parent && vnode.el) {
50+
parent.removeChild(vnode.el);
51+
}
52+
}
53+
function patchElement(n1, n2) {
54+
const el = n2.el = n1.el;
55+
const oldProps = n1.props;
56+
const newProps = n2.props;
57+
// 第一步:更新 props
58+
for (const key in newProps) {
59+
if (newProps[key] !== (oldProps === null || oldProps === void 0 ? void 0 : oldProps[key])) {
60+
el && patchProps(el, key, oldProps === null || oldProps === void 0 ? void 0 : oldProps[key], newProps[key]);
61+
}
62+
}
63+
for (const key in oldProps) {
64+
if (newProps && !(key in newProps)) {
65+
el && patchProps(el, key, oldProps[key], null);
66+
}
67+
}
68+
// 第二步:更新 children
69+
patchChildren(n1, n2, el);
70+
}
71+
function patchChildren(n1, n2, container) {
72+
// 判断新子节点的类型是否是文本节点
73+
if (typeof n2.children === 'string') {
74+
// 旧子节点的类型有三种可能:
75+
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
76+
if (Array.isArray(n1.children)) {
77+
// 这里涉及到diff算法
78+
n1.children.forEach((c) => unmount(c));
79+
}
80+
// 最后将新的文本节点内容设置给容器元素
81+
setElementText(container, n2.children);
82+
}
83+
else if (Array.isArray(n2.children)) {
84+
// 说明新子节点是一组子节点
85+
// 判断旧子节点是否也是一组子节点
86+
if (Array.isArray(n1.children)) {
87+
// 代码运行到这里,则说明新旧子节点都是一组子节点,这里涉及核心的 Diff 算法
88+
}
89+
else {
90+
// 此时:
91+
// 旧子节点要么是文本子节点,要么不存在
92+
// 但无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载
93+
setElementText(container, '');
94+
n2.children.forEach(c => patch(null, c, container));
95+
}
96+
}
97+
else {
98+
// 代码运行到这里,说明新子节点不存在
99+
// 旧子节点是一组子节点,只需逐个卸载即可
100+
if (Array.isArray(n1.children)) {
101+
n1.children.forEach(c => unmount(c));
102+
}
103+
else if (typeof n1.children === 'string') {
104+
// 旧子节点是文本子节点,清空内容即可
105+
setElementText(container, '');
106+
}
107+
// 如果也没有旧子节点,那么什么都不需要做
108+
}
109+
}
110+
function mountElement(vnode, container) {
111+
// 调用 createElement 函数创建元素
112+
const el = vnode.el = createElement(vnode.type);
113+
if (typeof vnode.children === 'string') {
114+
// 调用 setElementText 设置元素的文本节点
115+
setElementText(el, vnode.children);
116+
}
117+
else if (Array.isArray(vnode.children)) {
118+
// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它们
119+
vnode.children.forEach(child => {
120+
patch(null, child, el);
121+
});
122+
}
123+
if (vnode.props) {
124+
// 遍历 vnode.props
125+
for (const key in vnode.props) {
126+
patchProps(el, key, null, vnode.props[key]);
127+
}
128+
}
129+
// 调用 insert 函数将元素插入到容器内
130+
insert(el, container);
131+
}
132+
function patch(n1, n2, container) {
133+
if (typeof n2 === 'string')
134+
return;
135+
// 如果 n1 存在,则对比 n1 和 n2 的类型
136+
if (n1 && n1.type !== n2.type) {
137+
// 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
138+
unmount(n1);
139+
n1 = null;
140+
}
141+
// 代码运行到这里,证明 n1 和 n2 所描述的内容相同
142+
const { type } = n2;
143+
// 如果 n2.type 的值是字符串类型,则它描述的是普通标签元素
144+
if (typeof type === 'string') {
145+
if (!n1) {
146+
mountElement(n2, container);
147+
}
148+
else {
149+
patchElement(n1, n2);
150+
}
151+
}
152+
else if (type === selfText) { // 如果新 vnode 的类型是 Text,则说明该 vnode 描述的是文本节点
153+
// 如果没有旧节点,则进行挂载
154+
if (!n1) {
155+
// 使用 createTextNode 创建文本节点
156+
const el = n2.el = document.createTextNode(n2.children);
157+
// 将文本节点插入到容器中
158+
insert(el, container);
159+
}
160+
else {
161+
// 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
162+
const el = n2.el = n1.el;
163+
if (el && n2.children !== n1.children) {
164+
// setText(el, n2.children)
165+
el.nodeValue = n2.children;
166+
}
167+
}
168+
}
169+
else if (type === Fragment) { // 处理 Fragment 类型的 vnode
170+
if (!n1 && n2.children && Array.isArray(n2.children)) {
171+
// 如果旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可
172+
n2.children.forEach(c => patch(null, c, container));
173+
}
174+
else {
175+
// 如果旧 vnode 存在,则只需要更新 Fragment 的 children 即可
176+
patchChildren(n1, n2, container);
177+
}
178+
}
179+
else if (typeof type === 'object') {
180+
// 如果 n2.type 的值的类型是对象,则它描述的是组件
181+
}
182+
else if (['string', 'object'].includes(typeof type)) {
183+
// 处理其他类型的 vnode
184+
}
185+
}
186+
function render(vnode, container) {
187+
if (vnode) {
188+
// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
189+
patch(container._vnode, vnode, container);
190+
}
191+
else {
192+
if (container._vnode) {
193+
unmount(container._vnode);
194+
}
195+
}
196+
// 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
197+
container._vnode = vnode;
198+
}
199+
return {
200+
render
201+
};
202+
}
203+
// 在创建 renderer 时传入配置项
204+
const renderer = createRenderer({
205+
createElement(tag) {
206+
return document.createElement(tag);
207+
},
208+
setElementText(el, text) {
209+
el.textContent = text;
210+
},
211+
insert(el, parent, anchor) {
212+
parent.insertBefore(el, anchor);
213+
},
214+
createText(text) {
215+
return document.createTextNode(text);
216+
},
217+
setText(el, text) {
218+
el.nodeValue = text;
219+
},
220+
patchProps(el, key, prevValue, nextValue) {
221+
// 匹配以 on 开头的属性,视其为事件
222+
if (/^on/.test(key)) {
223+
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
224+
const invokers = el._vei || (el._vei = {});
225+
//根据事件名称获取 invoker
226+
let invoker = invokers[key];
227+
const name = key.slice(2).toLowerCase();
228+
if (nextValue) {
229+
if (!invoker) {
230+
// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
231+
// vei 是 vue event invoker 的首字母缩写
232+
invoker = el._vei[key] = (e) => {
233+
// 如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数
234+
// 如果事件发生的时间早于事件处理函数绑定的时间,则不处理执行事件处理函数
235+
if (e.timeStamp < invoker.attached)
236+
return;
237+
if (Array.isArray(invoker.value)) {
238+
invoker.value.forEach((fn) => fn(e));
239+
}
240+
else {
241+
// 否则直接作为函数调用
242+
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
243+
invoker.value(e);
244+
}
245+
};
246+
// 将真正的事件处理函数赋值给 invoker.value
247+
invoker.value = nextValue;
248+
// 添加 invoker.attached 属性,存储事件处理函数被绑定的时间
249+
invoker.attached = performance.now();
250+
// 绑定 invoker 作为事件处理函数
251+
el.addEventListener(name, invoker);
252+
}
253+
else {
254+
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
255+
invoker.value = nextValue;
256+
}
257+
}
258+
else if (invoker) {
259+
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
260+
el.removeEventListener(name, invoker);
261+
}
262+
}
263+
else if (key === 'class') {
264+
el.className = nextValue || '';
265+
}
266+
else if (shouldSetAsProps(el, key, nextValue)) {
267+
const type = typeof el[key];
268+
if (type === 'boolean' && nextValue === '') {
269+
el[key] = true;
270+
}
271+
else {
272+
el[key] = nextValue;
273+
}
274+
}
275+
else {
276+
el.setAttribute(key, nextValue);
277+
}
278+
}
279+
// 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
280+
});
281+
const selfText = Symbol();
282+
const Fragment = Symbol();
283+
const vnode = {
284+
type: Fragment,
285+
children: [
286+
{ type: 'li', children: 'text 1' },
287+
{ type: 'li', children: 'text 2' },
288+
{ type: 'li', children: 'text 3' }
289+
]
290+
};
291+
// 初次挂载
292+
renderer.render(vnode, document.querySelector('#app'));

0 commit comments

Comments
 (0)