Skip to content

Commit 73238d5

Browse files
committed
✨feat: 完善8.10文本节点和注释节点
1 parent 2304bd1 commit 73238d5

File tree

4 files changed

+702
-0
lines changed

4 files changed

+702
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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 (typeof type === 'object') {
170+
// 如果 n2.type 的值的类型是对象,则它描述的是组件
171+
}
172+
else if (['string', 'object'].includes(typeof type)) {
173+
// 处理其他类型的 vnode
174+
}
175+
}
176+
function render(vnode, container) {
177+
if (vnode) {
178+
// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
179+
patch(container._vnode, vnode, container);
180+
}
181+
else {
182+
if (container._vnode) {
183+
unmount(container._vnode);
184+
}
185+
}
186+
// 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
187+
container._vnode = vnode;
188+
}
189+
return {
190+
render
191+
};
192+
}
193+
// 在创建 renderer 时传入配置项
194+
const renderer = createRenderer({
195+
createElement(tag) {
196+
return document.createElement(tag);
197+
},
198+
setElementText(el, text) {
199+
el.textContent = text;
200+
},
201+
insert(el, parent, anchor) {
202+
parent.insertBefore(el, anchor);
203+
},
204+
createText(text) {
205+
return document.createTextNode(text);
206+
},
207+
setText(el, text) {
208+
el.nodeValue = text;
209+
},
210+
patchProps(el, key, prevValue, nextValue) {
211+
// 匹配以 on 开头的属性,视其为事件
212+
if (/^on/.test(key)) {
213+
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
214+
const invokers = el._vei || (el._vei = {});
215+
//根据事件名称获取 invoker
216+
let invoker = invokers[key];
217+
const name = key.slice(2).toLowerCase();
218+
if (nextValue) {
219+
if (!invoker) {
220+
// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
221+
// vei 是 vue event invoker 的首字母缩写
222+
invoker = el._vei[key] = (e) => {
223+
// 如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数
224+
// 如果事件发生的时间早于事件处理函数绑定的时间,则不处理执行事件处理函数
225+
if (e.timeStamp < invoker.attached)
226+
return;
227+
if (Array.isArray(invoker.value)) {
228+
invoker.value.forEach((fn) => fn(e));
229+
}
230+
else {
231+
// 否则直接作为函数调用
232+
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
233+
invoker.value(e);
234+
}
235+
};
236+
// 将真正的事件处理函数赋值给 invoker.value
237+
invoker.value = nextValue;
238+
// 添加 invoker.attached 属性,存储事件处理函数被绑定的时间
239+
invoker.attached = performance.now();
240+
// 绑定 invoker 作为事件处理函数
241+
el.addEventListener(name, invoker);
242+
}
243+
else {
244+
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value 的值即可
245+
invoker.value = nextValue;
246+
}
247+
}
248+
else if (invoker) {
249+
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
250+
el.removeEventListener(name, invoker);
251+
}
252+
}
253+
else if (key === 'class') {
254+
el.className = nextValue || '';
255+
}
256+
else if (shouldSetAsProps(el, key, nextValue)) {
257+
const type = typeof el[key];
258+
if (type === 'boolean' && nextValue === '') {
259+
el[key] = true;
260+
}
261+
else {
262+
el[key] = nextValue;
263+
}
264+
}
265+
else {
266+
el.setAttribute(key, nextValue);
267+
}
268+
}
269+
// 将属性设置相关操作封装到 patchProps 函数中,并作为渲染器选项传递
270+
});
271+
const selfText = Symbol();
272+
// 文本节点的 type 标识
273+
const textVNode = {
274+
// 描述文本节点
275+
type: selfText,
276+
children: '我是文本内容'
277+
};
278+
const selfComment = Symbol();
279+
// 注释节点的 type 标识
280+
const commentVNode = {
281+
// 描述注释节点
282+
type: selfComment,
283+
children: '我是注释内容'
284+
};
285+
// 初次挂载
286+
renderer.render(textVNode, document.querySelector('#app'));

0 commit comments

Comments
 (0)