Skip to content

Commit 2304bd1

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

File tree

3 files changed

+505
-213
lines changed

3 files changed

+505
-213
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<title>Title</title>
7+
</head>
8+
9+
<body>
10+
<div id="app"></div>
11+
<script>
12+
// propsKey 属性是否应该作为DOM Property被设置。
13+
// true 表示可以使用DOM Property设置,el[propsKey] = xx, false表示只可以用setAttribute设置
14+
function shouldSetAsProps (el, propsKey, value) {
15+
// 所有表单元素都具有form属性表示
16+
if (propsKey === 'form' && el.tagName.toLowerCase() === 'input')
17+
return false
18+
return propsKey in el
19+
}
20+
21+
// 渲染器
22+
function createRenderer (options = {}) {
23+
const {
24+
createElement,
25+
setElementText,
26+
insert,
27+
patchProps
28+
} = options
29+
30+
// 挂载函数
31+
function mountElement (vnode, container) {
32+
// 通过vnode的el属性获得真实DOM
33+
const el = vnode.el = createElement(vnode.type)
34+
// children处理
35+
if (typeof vnode.children === 'string') {
36+
// 如果节点值是string,设置文本
37+
setElementText(el, vnode.children)
38+
} else if (Array.isArray(vnode.children)) {
39+
// children是一个数组,遍历这个数组,数组每个元素都是一个vnode
40+
// 调用patch将他们挂载上去. oldVNode=null, container = el
41+
vnode.children.forEach(child => {
42+
patch(null, child, el)
43+
})
44+
}
45+
// 设置vnode属性
46+
if (vnode.props) {
47+
for (const propsKey in vnode.props) {
48+
const value = vnode.props[propsKey];
49+
patchProps(el, propsKey, null, value)
50+
}
51+
}
52+
// 将el挂载在container上
53+
insert(el, container)
54+
}
55+
56+
// 更新函数
57+
function patchElement (oldVNode, newVNode) {
58+
const el = newVNode.el = oldVNode.el,
59+
oldProps = oldVNode.props,
60+
newProps = newVNode.props
61+
// 更新Props
62+
for (const key in newProps) {
63+
if (newProps[key] !== oldProps[key]) {
64+
patchProps(el, key, oldProps[key], newProps[key])
65+
}
66+
}
67+
for (const key in oldProps) {
68+
if (!(key in newProps)) {
69+
patchProps(el, key, oldProps[key], null)
70+
}
71+
}
72+
// 更新子节点
73+
patchChildren(oldVNode, newVNode, el)
74+
}
75+
76+
function patchChildren (oldVNode, newVNode, container) {
77+
// 判断新子节点的类型是否是文本类型
78+
if (typeof newVNode.children === 'string') {
79+
// 旧子节点的类型只有三种: 无子节点, 文本子节点, 组合子节点
80+
if (Array.isArray(oldVNode.children)) {
81+
oldVNode.children.forEach(child => unmounted(child))
82+
}
83+
setElementText(container, newVNode.children)
84+
} else if (Array.isArray(newVNode.children)) { // 新子节点的类型是一组子节点
85+
if (Array.isArray(oldVNode.children)) { // 旧子节点的类型是一组节点
86+
// 核心Diff算法比较两个VNode子节点的区别
87+
// 此处是简单实现,并非Diff算法
88+
oldVNode.children.forEach(child => unmounted(child))
89+
newVNode.children.forEach(child => patch(null, child, container))
90+
} else {
91+
// 此时,旧子节点的类型要么是文本类型,要么是无
92+
// 但无论那种情况,都需要将容器清空,然后将新的一组子节点逐个挂载
93+
setElementText(container, '')
94+
newVNode.children.forEach(child => {
95+
patch(null, child, container)
96+
})
97+
}
98+
} else { // newVNode.children === null或者'' 没有新子节点,
99+
if (Array.isArray(oldVNode.children)) { // 旧子节点是一组节点 就逐个卸载
100+
oldVNode.children.forEach(child => unmounted(child))
101+
} else if (typeof oldVNode.children === 'string') { // 旧子节点是文本节点,就清空内容
102+
setElementText(container, '')
103+
}
104+
}
105+
}
106+
107+
// 补丁函数
108+
function patch (oldVNode, newVNode, container) {
109+
// oldVNode存在,新旧vnode的type不同
110+
if (oldVNode && oldVNode.type !== newVNode.type) {
111+
unmounted(oldVNode)
112+
oldVNode = null
113+
}
114+
// 代码运行到这说明oldVNode和newVNode的type是相同的
115+
const { type } = newVNode
116+
// newVNode type是string, 说明是普通标签元素. 如果type是object,则描述的是组件
117+
if (typeof type === 'string') {
118+
if (!oldVNode) {
119+
// 挂载
120+
mountElement(newVNode, container)
121+
} else {
122+
patchElement(oldVNode, newVNode)
123+
}
124+
} else if (typeof type === 'object') {
125+
// 组件类型的VNode
126+
} else if (type === 'xxx') {
127+
// 其他类型的VNode
128+
}
129+
130+
}
131+
132+
// 卸载函数
133+
function unmounted (vnode) {
134+
const parent = vnode.el.parentNode
135+
if (parent) parent.removeChild(vnode.el)
136+
}
137+
138+
// 渲染函数
139+
function render (vnode, container) {
140+
if (vnode) {
141+
// 新vnode存在,将其与旧vnode一起传递给patch函数打补丁
142+
patch(container._vnode, vnode, container)
143+
} else {
144+
if (container._vnode) {
145+
// 新vnode不存在,旧vnode存在,说明是卸载(unmount)操作
146+
// 清空DOM
147+
unmounted(container._vnode)
148+
}
149+
}
150+
container._vnode = vnode // vnode改成旧的
151+
}
152+
153+
// 同构渲染
154+
function hydrate (vnode, container) {
155+
156+
}
157+
158+
return {
159+
render,
160+
hydrate
161+
}
162+
}
163+
164+
// normalizeClass里处理Object.
165+
function handleObject (set, obj) {
166+
for (const key in obj) {
167+
if (obj[key]) set.add(key) // 如果对象的值为true, set里把键(className)加进去
168+
}
169+
}
170+
171+
function normalizeClass (classValue) {
172+
// 字符串
173+
if (typeof classValue === 'string') return classValue
174+
let resultClassSet = new Set()
175+
// 数组和对象情况
176+
if (Array.isArray(classValue)) {
177+
for (const value of classValue) {
178+
if (typeof value === 'string') resultClassSet.add(value)
179+
else {
180+
// 对象
181+
handleObject(resultClassSet, value)
182+
}
183+
}
184+
} else {
185+
// 对象
186+
handleObject(resultClassSet, classValue)
187+
}
188+
return Array.from(resultClassSet).join(' ').trim()
189+
}
190+
191+
const vnode = {
192+
type: 'div',
193+
props: {
194+
id: 'foo',
195+
class: normalizeClass([
196+
'ds',
197+
{
198+
test1: true,
199+
test2: false,
200+
test3: true
201+
}
202+
]),
203+
onClick: [
204+
() => {
205+
console.log(1)
206+
},
207+
() => {
208+
console.log(2)
209+
}
210+
]
211+
},
212+
children: [
213+
{
214+
type: 'h1',
215+
children: 'hello h1'
216+
}
217+
]
218+
}
219+
const renderer = createRenderer({
220+
createElement (tag) {
221+
return document.createElement(tag)
222+
},
223+
setElementText (el, text) {
224+
el.textContent = text
225+
},
226+
insert (el, parent, anchor = null) {
227+
parent.insertBefore(el, anchor)
228+
},
229+
patchProps (el, propsKey, previousValue, nextValue) {
230+
if (/^on/.test(propsKey)) { // on开头的属性是事件
231+
const invokers = el._vei || (el._vei = {})
232+
// 根据事件名获取invoker
233+
let invoker = invokers[propsKey]
234+
const eventName = propsKey.slice(2).toLowerCase()
235+
if (nextValue) {
236+
if (!invoker) { // invoker不存在
237+
invoker = el._vei[propsKey] = (event) => { // 伪造的事件处理函数放到el._vei[propsKey]中
238+
if (event.timestamp < invoker.attached) return; // 事件发生的时间 早于 事件处理函数绑定的时间,则不执行事件处理函数
239+
if (Array.isArray(invoker.value)) { // invoker.value 是一个数组
240+
invoker.value.forEach(fn => fn(event))
241+
} else {
242+
invoker.value(event) // 当伪造事件处理函数invoker执行的时候,会执行内部真正的事件处理函数invoker.value,传递event进去
243+
}
244+
}
245+
invoker.value = nextValue // 真正的事件处理函数赋值给invoker.value
246+
invokers.attached = performance.now() // 事件绑定时间
247+
el.addEventListener(eventName, invoker)
248+
} else {
249+
// invoker存在,nextValue存在,那就是更新。把伪造事件处理函数invoker的value更新
250+
invoker.value = nextValue
251+
}
252+
} else if (invoker) { // nextValue不存在,而invoker存在,说明要把原来的事件移除绑定
253+
el.removeEventListener(eventName, invoker)
254+
}
255+
}
256+
if (propsKey === ' class') {
257+
el.className = nextValue
258+
} else if (shouldSetAsProps(el, propsKey, nextValue)) { // HTML Attribute 跟 DOM Property有对应
259+
const type = typeof el[propsKey]
260+
261+
// 如果type 是布尔类型的值而且value是空字符串,说明应该设置为true,而不是false
262+
if (type === 'boolean' && nextValue === '') el[propsKey] = true // 矫正设置DOM Property
263+
else el[propsKey] = nextValue // 设置DOM Property
264+
265+
} else { // HTML Attribute 和 DOM Property 没对应,比如class 和 className
266+
el.setAttribute(propsKey, vnode.props[propsKey])
267+
}
268+
}
269+
})
270+
renderer.render(vnode, document.getElementById('app'))
271+
272+
</script>
273+
</body>
274+
275+
</html>

0 commit comments

Comments
 (0)