第八章、挂载与更新
元素可以有多个子元素,及 children 属性会是数组。
const vnode = {
type: 'div',
children: [
{
type: 'p',
children: 'hello',
},
],
}
function mountElement(vnode, container) {
const el = createElement(vnode.type)
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
// 当children属性是一个数组的时候,表示有多个子元素,遍历调用patch进行挂载
vnode.children.forEach((child) => {
// 因为mountElement是挂载阶段,所以没有旧vnode,n1传null
patch(null, child, el)
})
}
insert(el, container)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
vnode 节点可以通过 props 属性描述它的属性。但是元素的属性有 HTML 属性和 DOM 属性之分,HTML 属性的作用是用来设置与之对应的 DOM 属性的初始值,但是在某些特殊属性的时候要做对应的处理。
// 某些属性是只读的,不需要更新,例如input框上的form属性,做特殊处理
function shouldSetAsProps(el, key, value) {
if (key === 'form' && el.tagName === 'INPUT') return false
return key in el
}
function mountElement(vnode, container) {
const el = createElement(vnode.type)
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
// 判断当前属性是否需要更新
if (shouldSetAsProps(el, key, value)) {
const type = typeof el[key]
// 有些属性是布尔值类型的,例如 <button disabled>按钮</button>
// 这些属性在模板上可以只写名称不写值,会被编译为 disabled: ""
// 要对这些属性做特殊处理,将编译后的空字符串转为布尔值
if (type === 'boolean' && value === '') {
el[key] = true
} else {
el[key] = value
}
} else {
// 没有对应的DOM属性的时候就使用setAttribute更新属性
el.setAttribute(key, value)
}
}
}
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach((child) => {
patch(null, child, el)
})
}
insert(el, container)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
同样设置属性的函数也可以提取出来有渲染器传入。
const renderer = createRenderer({
patchProps(el, key, prevValue, nextValue) {
if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 没有对应的DOM属性的时候就使用setAttribute更新属性
el.setAttribute(key, nextValue)
}
},
})
function mountElement(vnode, container) {
// ----
if (vnode.props) {
for (const key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
// ----
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class 也是一个特殊的属性,vue 的 class 支持字符串,数组,对象多种格式,会做统一的转换。style 属性也是类似。
// 统一格式化class
function normalizeClass(value) {
let res = ''
if (typeof value === 'string') {
res = value
} else if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i])
if (normalized) {
res += normalized + ' '
}
}
} else if (value !== null && typeof value === 'object') {
for (const name in value) {
if (value[name]) {
res += name + ' '
}
}
}
return res.trim()
}
// 编译成vnode的时候调用格式化class
const vnode = {
type: 'div',
props: {
class: normalizeClass(['foo bar', { baz: true }]), // 格式化的结果将是 'foo bar baz'
},
children: [
{
type: 'p',
children: 'hello',
},
],
}
// 更新props的时候对class特殊处理
const renderer = createRenderer({
patchProps(el, key, prevValue, nextValue) {
if (key === 'class') {
// el.className 的方式设置class性能最优
el.className = nextValue || ''
} else if (key === 'style') {
// key 为 style 时做跟class类似的特殊处理
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
el.setAttribute(key, nextValue)
}
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
当后续渲染的 n2 传入的值为 null 时,表示本次什么都不需要渲染,所以需要将之前的渲染内容卸载,封装统一的卸载方法。
function mountElement(vnode, container) {
// 将创建好的节点挂载在vnode.el上,方便之后引用
const el = (vnode.el = createElement(vnode.type))
// ----
}
const renderer = createRenderer({
// 自定义卸载方法传入
unmount(vnode) {
// 找到父节点,调用父节点的removeChild进行删除
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
},
})
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container)
} else {
// 执行我们传入的卸载节点的方法
if (container._vnode) {
unmount(container._vnode)
}
}
container._vnode = vnode
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
现在我们需要对 vnode 的 type 属性作区分,对不同类型的 vnode 要做不同的处理。
function patch(n1, n2, container) {
// 如果新旧节点的type不一样,表示两个节点已经完全不一样了,例如从p标签改为了div标签。
// 节点类型不同没有对比的意义,直接卸载旧的节点再挂载新的节点就好
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
// 因为我们在之前的render函数已经判断了新节点是否存在,所以执行到这里的时候表示n2一定是存在的
// 判断新节点的类型,根据不同类型做不同的处理
const { type } = n2
// string 类型表示n2是普通的标签元素
if (typeof type === 'string') {
// 旧节点存在则执行更新操作,不存在就直接绑定新节点
if (!n1) {
mountElement(n2, container)
} else {
// 更新节点
patchElement(n1, n2)
}
} else if (typeof type === 'object') {
// object 类型表示新节点是一个组件
} else if (typeof type === 'xxx') {
// 执行其他类型的操作
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
我们约定以 on 开头的属性表示事件,当碰到以 on 开头的属性时,要给元素绑定事件。
// 编译成vnode的时候调用格式化class
const vnode = {
type: 'div',
props: {
// on开头的属性是一个事件
onClick: () => {
console.log('click')
},
},
children: 'text',
}
const renderer = createRenderer({
patchProps(el, key, prevValue, nextValue) {
// 判断当前属性是否为一个事件
if (/^on/.test(key)) {
// 取出事件名称 onClick --> click
const name = key.slice(2).toLowerCase()
// 绑定事件
el.addEventListener(name, nextValue)
} else if (key === 'class') {
el.className = nextValue || ''
} else if (key === 'style') {
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
el.setAttribute(key, nextValue)
}
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
当卸载节点的时候我们要移除之前绑定的事件,频繁的绑定和移除事件太消耗性能了,需要一种更优雅的方式来处理。
const renderer = createRenderer({
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
// 定义一个 invoker 来伪造我们的事件处理函数,直接从el._vei上去值,我们之后会给这个属性赋值,第一次绑定函数这个值会不存在
let invoker = el._vei
const name = key.slice(2).toLowerCase()
if (nextValue) {
// 新值存在且伪造函数不存在,表示我们是第一次绑定函数,就为我们的伪造函数赋值并储存在el._vei上
if (!invoker) {
invoker = el._vei = (e) => {
invoker.value(e)
}
// invoker.value是我们最终执行的事件处理函数,每次更新的时候只要更新
invoker.value = nextValue
el.addEventListener(name, invoker)
} else {
// 这里表示我们之前绑定过伪造的事件处理函数,只要用新值更新我们的伪造函数就好,避免了频繁的绑定和移除事件的操作
invoker.value = nextValue
}
} else if (invoker) {
// 这里表示新值不存在,但是之前有绑定过伪造的事件处理函数,直接移除就好
el.removeEventListener(name, invoker)
}
} else if (key === 'class') {
el.className = nextValue || ''
} else if (key === 'style') {
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
el.setAttribute(key, nextValue)
}
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
目前我们的 el._vel
属性只能储存单一的事件处理函数,但是元素节点是可以有多个事件,每个事件都可以有多个事件处理函数的。
const vnode = {
type: 'div',
// 多个事件,且每个事件都可能有多个事件处理函数
props: {
onClick: [
() => {
console.log('click1')
},
() => {
console.log('click2')
},
],
onContextmenu: () => {
console.log('contextmenu')
},
},
children: 'text',
}
const renderer = createRenderer({
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
// 定义invokers表示绑定的所有事件集合
const invokers = el._vei || (el._vei = {})
// 根据当前key取出对应的事件处理函数
let invoker = invokers[key]
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
// 事件处理函数储存在对应的key上
invoker = el._vei[key] = (e) => {
// 当前事件如果有多个事件处理函数,则遍历执行
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
invoker.value(e)
}
}
invoker.value = nextValue
el.addEventListener(name, invoker)
} else {
invoker.value = nextValue
}
} else if (invoker) {
el.removeEventListener(name, invoker)
}
} else if (key === 'class') {
el.className = nextValue || ''
} else if (key === 'style') {
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
el.setAttribute(key, nextValue)
}
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
有时候事件的冒泡和渲染函数的执行会导致绑定的事件错误执行,需要对触发事件的时间和绑定事件的时间做处理。
const bol = ref(false)
effect(() => {
const vnode = {
type: 'div',
// 初始时 bol.value 值为false,div不会被绑定click事件,子元素p绑定了click事件
// 触发p元素的click事件,会发现div元素的事件也触发了,因为p元素改变了bol.value的值,触发了重新渲染
// 重新渲染给div元素绑定了事件,当p元素的事件冒泡到div元素时,div元素已经被绑定了事件处理函数了
// 需要对 触发事件 和 绑定事件 两者的时间做处理
props: bol.value
? {
onClick: () => {
console.log('div')
},
}
: {},
children: [
{
type: 'p',
props: {
onClick: () => {
bol.value = true
},
},
children: 'text',
},
],
}
renderer.render(vnode, document.querySelector('#app'))
})
const renderer = createRenderer({
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
const invokers = el._vei || (el._vei = {})
let invoker = invokers[key]
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
invoker = el._vei[key] = (e) => {
// 当前任务触发的时间如果早于绑定的时间,就不执行事件回调函数了
if (e.timeStamp < invoker.attached) return
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
invoker.value(e)
}
}
invoker.value = nextValue
// 记录一下当前事件绑定的时间
invoker.attached = performance.now()
el.addEventListener(name, invoker)
} else {
invoker.value = nextValue
}
} else if (invoker) {
el.removeEventListener(name, invoker)
}
} else if (key === 'class') {
el.className = nextValue || ''
} else if (key === 'style') {
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
el.setAttribute(key, nextValue)
}
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
更新子节点的时候,有 没有子节点 文本子节点 一组子节点 3 种情况(单一子节点也属于一组子节点,只是子节点数量为 1),这 3 种情况互相转换会产生 3 * 3 = 9 种结果,但实际代码中并不需要完全覆盖 9 种结果。
// 更新节点
function patchElement(n1, n2) {
// 取出旧节点,
const el = (n2.el = n1.el)
const oldProps = n1.props
const newProps = n2.props
// 先遍历新属性,依照新属性的key对旧属性进行更新
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)) {
patchProps(el, key, oldProps[key], null)
}
}
// 更新子节点
patchChildren(n1, n2, el)
}
function patchChildren(n1, n2, container) {
// 新子节点为文本节点的时候
if (typeof n2.children === 'string') {
// 旧子节点会有3种可能,没有节点、文本节点、一组子节点,但是只有是一组子节点的时候,需要逐个移除
if (Array.isArray(n1.children)) {
n1.children.forEach((child) => {
unmount(child)
})
}
// 其他的情况直接设置文本就可以了
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
// 新子节点是一组子节点的时候
if (Array.isArray(n1.children)) {
// 如果旧子节点也是一组子节点,这里需要进入核心的diff逻辑
} else {
// 其他的情况只需要清空文本,然后挂载新子节点
setElementText(container, '')
n2.children.forEach((child) => {
patch(null, child, container)
})
}
} else {
// 运行到这一步说明新子节点不存在
if (Array.isArray(n1.children)) {
// 如果旧子节点是一组节点,就逐个卸载
n1.children.forEach((child) => {
unmount(child)
})
} else if (typeof n1.children === 'string') {
// 如果旧子节点是文本,则清空文本
setElementText(container, '')
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
目前我们只处理了普通标签类型的 vnode,vnode 的 type 会有各种各样的,下面处理一下文本节点和注释节点的情况
// 定义新的Symbol值来表示节点的类型
const Text = Symbol()
const Comment = Symbol()
// type属性表示当前vnode的类型
const textVnode = {
type: Text,
children: '文本内容',
}
const commentVnode = {
type: Comment,
children: '注释内容',
}
// 同样操作节点的方法由外部传入
const renderer = createRenderer({
createText(text) {
return document.createTextNode(text)
},
setText(el, text) {
el.nodeValue = text
},
})
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
if (!n1) {
mountElement(n2, container)
} else {
patchElement(n1, n2)
}
} else if (typeof type === 'object') {
} else if (type === Text) {
// Text 类型表示文本节点
if (!n1) {
// 旧节点不存在则直接挂载新节点
const el = (n2.el = createText(n2.children))
insert(container, el)
} else {
// 旧节点存在,且新旧文本内容不一致,则修改节点文本
const el = (n2.el = n1.el)
if (n2.children !== n1.children) {
setText(el, n2.children)
}
}
}
}
// 注释节点类型操作与文本节点类似,只不过创建注释节点是用 document.createComment() 方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
vue3 在一个 template 下可以有多个子节点了,需要我们用一个 Fragment 类型来表示这种情况。
const Fragment = Symbol()
const fragmentVnode = {
type: Fragment,
children: [
{ type: 'li', children: 'text1' },
{ type: 'li', children: 'text2' },
{ type: 'li', children: 'text3' },
],
}
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
if (!n1) {
mountElement(n2, container)
} else {
patchElement(n1, n2)
}
} else if (typeof type === 'object') {
} else if (type === Text) {
if (!n1) {
const el = (n2.el = createText(n2.children))
insert(container, el)
} else {
const el = (n2.el = n1.el)
if (n2.children !== n1.children) {
setText(el, n2.children)
}
}
} else if (type === Fragment) {
// Fragment本身不渲染任何东西,只要对它的子节点做处理就好了
if (!n1) {
// 旧vnode不存在,直接挂载Fragment的children
n2.children.forEach((child) => patch(null, child, container))
} else {
// 旧vnode存在,更新子节点
patchChildren(n1, n2, container)
}
}
}
const renderer = createRenderer({
unmount(vnode) {
// 卸载的函数要对 Fragment 做处理,只要卸载它的子节点就好了
if (vnode.type === Fragment) {
vnode.children.forEach((child) => unmount(child))
return
}
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59