render,h的传参

介绍

除了template,还可以为组件提供render函数,h(helper)作为渲染函数的参数,可以创造vnode,而mount可以把vnode挂载为真实dom结点

vnode接收的三个参数:

  1. 类型,如’div’

  2. 对象,包含vnode上的所有数据、属性…

  3. 子结点

    • 直接传递一个字符串,如’hello’,表明这是一个文本子结点
    • 也可传递一个包含更多子结点的数组,嵌套更多的嵌套的h调用

例子

1
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
<script src="https://unpkg.com/vue@next"></script>
<style>
.mt-4 {
margin: 10px;
}

.my-nav{
margin: 10px;
}
</style>

<div id="app">
<Stack size="4">
<div>hello</div>
<Stack size="4">
<div>hello</div>
<div>hello</div>
</Stack>
</Stack>
</div>

<script>
const { h, createApp } = Vue;
const Stack = {
render() {
const slot = this.$slots.default
? this.$slots.default()
: []

// 尤大的例子,使用了插槽:
return h('div', { class: 'stack' }, slot.map(child => {
// 对Stack里的每一个子元素,都套上一层div,class为mt-4
// 在这个div中,把原先的子元素重复三次,再放进去
return h('div', { class: `mt-${this.$attrs.size}`}, [
child, child, child,
])
}))

// return里使用vnode的三个参数:
return h('nav', { class: 'my-nav'}, [
h('nav', { class: 'my-nav'}, 'This is my nav 0'),
h('nav', { class: 'my-nav'}, [
'This is my nav 1',
h('nav', { class: 'my-nav'}, 'This is my nav 2'),
h('nav', { class: 'my-nav'}, 'This is my nav 3'),
h('nav', { class: 'my-nav'}, 'This is my nav 4'),
]),
])
}
}

const App = {
components: {
Stack
}
// 此处不提供template选项,则默认使用dom内模板,即上方的html
}

createApp(App).mount('#app');
</script>

h的实现

h函数的实现很简单,仅仅是把三个参数放入一个对象:

1
2
3
4
5
6
7
function h(tag, props, children) {
return {
tag,
props,
children
}
}

mount的实现

创建结点,设置属性,插入孩子

1
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
<div id="app"></div>

<script>
function h(tag, props, children) {
// ...
}
// 简化版mount
function mount(vnode, container) {
// 使用 vnode.el 存储dom元素,所以看到以前的vdom时,可以使用.el访问旧的真实dom树(与patch配合完成dom树的修改)
const el = vnode.el = document.createElement(vnode.tag)
// props —— 假设只用考虑attrs
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
// children —— 假设孩子都是数组
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => {
mount(child, el)
})
}
}
container.appendChild(el)
}

const vdom = h('div', { class: 'red' }, [h('span', null, 'hello')])

mount(vdom, document.getElementById('app'))
</script>

patch

介绍

  • 页面已经完成一次渲染,h函数生成vdom;

  • 在一个响应式属性被更新时,触发重新渲染,重新生成vdom;

  • 现需要对两个vdom进行比较,依赖patch函数,使dom反映更新后的状态

1
2
3
4
5
6
7
8
9
const vdom = h('div', { class: 'red' }, [
h('span', null, 'hello')
])

const vdom2 = h('div', { class: 'green' }, [
h('span', null, 'changed!')
])

patch(vdom, vdom2)

实现

1
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
75
76
77
78
79
80
81
82
83
84
85
86
<div id="app"></div>

<style>
.red {
color: red;
}
.green {
color: green;
}
</style>

<script>
function h(tag, props, children) {
return {
tag,
props,
children,
}
}
function mount(vnode, container) {
// ...
}

const vdom = h('div', { class: 'red' }, [h('span', null, 'hello')])
mount(vdom, document.getElementById('app'))

function patch(n1, n2) {
if (n1.tag === n2.tag) {
const el = (n2.el = n1.el)
// props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
el.setAttribute(key, newValue)
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}

// children
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(child => {
mount(child, el)
})
} else {
oldChildren.slice(oldChildren.length).forEach(child => {
el.removeChild(child.el)
})
}
}
}
}
}

const vdom2 = h('div', { class: 'green' }, [h('span', null, 'hello')]);
patch(vdom, vdom2) // 原本字体为红色,刷新后字体为绿色

</script>

(补充)Vue3响应式原理

Reactivity

本节主要讲述存储不同effect的方法,但还不能让effect自动重新运行

dep

effect函数可以计算出我们想要得到的结果,track函数用于将各个effect保存在dep集合中,trigger函数用于重新调用每个effect函数,更新值从而实现响应式

这里是第一步,将所有effect放入一个集合,trigger更新时将重新触发所有effect,有可能某个effect和quantity是无关的,却要再触发一次,这显然可以改进

depsMap

第二步,创建一个depsMap,可以保存对应不同属性的依赖(即effect函数),当某个属性改变,trigger时只需要重新触发和它有关的effect函数即可。

比如此处的effect和quantity有关,假如还有一个effect可计算出quantity * 10,那么它也会保存在quantity对应的dep中,在trigger执行时更新

现在的问题是,我们可能有多个响应式对象,它们含有不同的属性(可能有同名的属性),因此在外边还需要一层targetMap

targetMap

weakMap的键可以是一个对象(此处保存每一个响应式对象),值对应之前的depsMap

targetMap存储了每个响应式对象的依赖,depsMap存储了每个属性的依赖,dep是一个effects集的依赖

Proxy and Reflect

使用ES6的proxy和reflect,读取属性时自动调用track,修改属性时自动调用trigger

了解:receiver保证了,当我们的对象有继承自其他对象的值或函数时,this指针能正确地指向使用的对象,避免一些在vue2中的响应式警告

activeEffect

在之前的代码中,只要读取某个值,就会调用get中的track函数,遍历targetMap和各种依赖,这样会增加不必要的开销

activeEffect表示正在运行中的effect,只有当存在activeEffect时,再遍历targetMap

视频作者说:“应当在effect中调用track函数”,我认为这样是正确的,在effect函数执行时,activeEffect不为null,此时调用track显得理所当然,但是代码中却没有体现对track的调用,activeEffect一下子又被设置为null。按照之前一节的理解,track在读取属性时被调用,在下方console.log时activeEffect已经变成null了,那track了个寂寞?

可能是这样的:在total = product.price * product.quantity 这一行,相当于已经访问了total,在此处调用了track,目前我只能这样理解了

ref

上方介绍了reactive的实现,此处介绍ref:

ref可以把某个变量变成响应式,它无需作为某个对象的属性

从零构建响应式

以下的两节,和上方的Vue3响应式原理大致相同

1
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
let activeEffect

class Dep {
constructor(value) {
this.subscribers = new Set()
this._value = value
}
get value() {
this.depend() // track
return this._value
}
set value(newValue) {
this._value = newValue
this.notify() // trigger
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}

function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}

const ok = new Dep(true)
const msg = new Dep('hello')

watchEffect(() => {
if (ok.value) {
console.log(msg.value)
} else {
console.log('false branch')
}
})

msg.value = 'changed' // 触发set中的notify函数,重新执行effect

// 控制台打印结果:
// 'hello'
// 'changed'

从零构建 Reactive

1
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
let activeEffect

class Dep {
subscribers = new Set()
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}

function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}

const targetMap = new WeakMap()

function getDep(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}

const reactiveHandlers = {
get(target, key, receiver) {
const dep = getDep(target, key)
dep.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const dep = getDep(target, key)
const result = Reflect.set(target, key, value, receiver)
dep.notify()
return result
},
}

function reactive(raw) {
return new Proxy(raw, reactiveHandlers)
}

const state = reactive({
count: 0
})

state.count++;

构建Vue

vdom + reactive 即构建出了一个小型的Vue,现在的场景是:点击组件实现count值增加,并响应式地体现在页面上:

1
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<div id="app"></div>

<script>
// vdom
function h(tag, props, children) {
return {
tag,
props,
children,
}
}

function mount(vnode, container) {
const { tag, props, children } = vnode
const el = (vnode.el = document.createElement(tag))
if (props) {
for (let key in props) {
const value = props[key]
if (key.startsWith('on')) {
// 监听事件
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
if (children) {
if (Array.isArray(children)) {
children.forEach(child => {
if (typeof child === 'string') {
el.append(child)
} else if (typeof child === 'object') {
mount(child, el)
}
})
} else {
el.append(children)
}
}
container.append(el)
}

function patch(n1, n2) {
if (n1.tag === n2.tag) {
const el = (n2.el = n1.el)
//diff props

const oldProps = n1.props || {}
const newProps = n2.props || {}
//添加新的属性或更改原来已有但变化了的属性
for (let key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
el.setAttribute(key, newValue)
}
}

//移除新属性中没有的属性
for (let key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}

//diff children
const oldChildren = n1.children
const newChildren = n2.children

if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
if (oldChildren !== newChildren) {
el.innerHTML = newChildren
}
}
} else if (
typeof oldChildren === 'string' &&
Array.isArray(newChildren)
) {
el.innerHTML = ''
newChildren.forEach(child => mount(child, el))
} else if (Array.isArray(oldChildren) && Array.isArray(newChildren)) {
const minLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < minLength; i++) {
patch(oldChildren[i], newChildren[i])
}
if (oldChildren.length === minLength) {
for (let i = minLength; i < newChildren.length; i++) {
mount(newChildren[i], el)
}
} else {
for (let i = minLength; i < oldChildren.length; i++) {
el.removeChild(oldChildren[i].el)
}
}
}
} else {
//replace
}
}

// reactivity
let activeEffect = null

class Dep {
subs = new Set()
depend() {
if (activeEffect) {
this.subs.add(activeEffect)
}
}
notify() {
this.subs.forEach(sub => sub())
}
}

function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}

const targetMap = new WeakMap()

function getDep(target, key) {
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
const depMap = targetMap.get(target)
if (!depMap.has(key)) {
depMap.set(key, new Dep())
}
return depMap.get(key)
}

const reactiveHandlers = {
get(target, key, receiver) {
const dep = getDep(target, key)
dep.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const dep = getDep(target, key)
const ret = Reflect.set(target, key, value, receiver)
dep.notify()
return ret
},
}

function reactive(raw) {
return new Proxy(raw, reactiveHandlers)
}

// component组件实例,container要挂载的dom元素
function mountApp(component, container) {
let isMounted = false
let oldVdom
watchEffect(() => {
if (!isMounted) {
//第一次挂载
oldVdom = component.render()
mount(oldVdom, container)
isMounted = true
} else {
//数据变化,要进行更新
const newVdom = component.render()
patch(oldVdom, newVdom)
oldVdom = newVdom
}
})
}

const App = {
data: reactive({
count: 0,
}),
render() {
return h('div', null, [
h(
'div',
{
onClick: () => App.data.count++,
},
String(this.data.count)
),
])
},
}
//一个点击自增的计数器
mountApp(App, document.getElementById('app'))
</script>