第十二章、组件的实现原理
组件的本质是一个选项对象,他包含了组件的各种数据配置,在 vnode 中,这个选项对象会储存在 type 属性上。
const Mycomponent = {
name: 'Mycomponent',
data() {
return {
foo: 'hello world',
}
},
// 组件模板会被编译为render函数
render() {
return {
type: 'div',
children: `foo的值为${this.foo}`,
}
},
}
// 编译为vnode
const compVnode = {
type: Mycomponent,
}
// 渲染
renderer.render(compVnode, document.querySelector('#app'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
组件的 vnode 拥有特殊的 type 属性,在 patch 的时候对其做特殊处理。
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
///
} else if (typeof type === 'object') {
// type的类型是object的话表示是一个组件
if (!n1) {
// 绑定组件
mountComponent(n2, container)
} else {
// 更新组件
patchComponent(n1, n2)
}
} else if (type === Text) {
///
} else if (type === Fragment) {
///
}
}
// 同样使用一个微任务队列来执行渲染,防止数据的频繁变动导致的渲染函数多次调用
const queue = new Set()
let isFlushing = false
const p = Promise.resolve()
function queueJob(job) {
queue.add(job)
if (!isFlushing) {
isFlushing = true
p.then(() => {
try {
queue.forEach((job) => job())
} finally {
isFlushing = false
queue.clear()
}
})
}
}
function mountComponent(vnode, container) {
// 取出组件的配置信息
const componentOptions = vnode.type
const { render, data } = componentOptions
// 将组件数据转化为响应式数据
const state = reactive(data())
// 使用effect包装渲染任务,这样响应式数据变化后,重新执行副作用函数渲染组件
effect(
() => {
// call绑定渲染函数执行时的this指向,获取render函数返回的vnode进行渲染
const subTree = render.call(state, state)
patch(null, subTree, container)
},
{
// 自定义调度函数
scheduler: queueJob,
}
)
}
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
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
目前我们的 mountComponent 函数内部在调用 patch 函数渲染时,传入的第一个参数总是 null,也就是说每次更新都会是全新的挂载,而不会进行补丁操作。所以我们需要实现一个组件的实例,这个实例用来维护整个组件的生命周期。
function mountComponent(vnode, container) {
const componentOptions = vnode.type
// 取出组件的各种生命周期
const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions
// 执行创建组件实例前的生命周期
beforeCreate && beforeCreate()
const state = reactive(data())
// 定义组件实例,上面有各种表示当前组件状态的属性
const instance = {
state,
isMounted: false,
subTree: null,
}
// 将实例挂载到vnode上面,方便之后其他地方访问
vnode.component = instance
// 创建组件完成的生命周期,注意在这个生命周期开始,可以访问state上面的数据了,所以要绑定一下其中this的指向,之后的生命周期也一样
created && created.call(state)
effect(
() => {
const subTree = render.call(state, state)
// 根据isMounted属性判断当前是挂载还是更新,并执行对应的生命周期
if (!instance.isMounted) {
beforeMount && beforeMount.call(state)
patch(null, subTree, container)
instance.isMounted = true
mounted && mounted.call(state)
} else {
beforeUpdate && beforeUpdate.call(state)
patch(instance.subTree, subTree, container)
updated && updated.call(state)
}
// 渲染完成后更新实例上面的subTree
instance.subTree = subTree
},
{
scheduler: queueJob,
}
)
}
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
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
组件会显式的指定需要接收的 props,但是调用组件的时候也可以传组件没有指定的 props,这需要我们对其做区分。
// 使用组件时可能传多个props
// <MyComponent title="this is titile" :other="val" />
// 上面的模板会被编译为这样的vnode
const compVnode = {
type: Mycomponent,
props: {
title: 'this is title',
other: this.val,
},
}
// 在组件内部,只显式的接收了title属性
const Mycomponent = {
name: 'Mycomponent',
props: {
// 只接收了title属性,而且规定为string类型
title: String,
},
render() {
return {
type: 'div',
children: `title的值为${this.title}`,
}
},
}
// 存在两个props属性,一个是vnode上的,也就是我们实际使用组件时传入的,一个是组件描述对象上的,也就是组件实际接收的
// 我们要对这两个props做处理,只有定义在组件内部的props,才是组件需要的,其他的props将其转换为attrs
function mountComponent(vnode, container) {
const componentOptions = vnode.type
// 取出组件上的props属性
const { props: propsOptions } = componentOptions
///
// 将其与vnode上的props进行格式化
const [props, attrs] = resolveProps(propsOptions, vnode.props)
const instance = {
state,
isMounted: false,
// props包装成浅响应数据
props: shallowReactive(props),
subTree: null,
}
///
}
// 格式化props,将其区分为组件实际接收的props和attrs
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if (key in options) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
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
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
父节点的 props 变化的时候,会导致父组件的更新,渲染器更新父节点的子节点时,会发现包含组件类型的子节点,就会执行 patchComponent 操作。
function patchComponent(n1, n2) {
// 获取conponent实例,并更新到n2上
const instance = (n2.component = n1.component)
// 取出props属性
const { props } = instance
// 判断props属性是否需要更新
if (hasPropsChanged(n1.props, n2.props)) {
// 分别从n2的组件对象和n2的vnode上取出props属性,重新格式化
const [nextProps] = resolveProps(n2.type.props, n2.props)
// 更新props
for (const key in nextProps) {
props[key] = nextProps[key]
}
// 已经没有的props删除掉
for (const key in props) {
if (!(key in nextProps)) delete props[key]
}
}
}
// 判断props属性是否需要更新
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps)
// key的长度不一样了,表示有新增或者减少key,需要更新
if (nextKeys.length !== Object.keys(propsData).length) {
return true
}
// 长度一致则判断key的值是否相同,不同则需要更新
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) return true
}
return false
}
// attrs属性的更新和props类似
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
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
因为我们可以通过 this.xxx 同时访问定义在 props 属性和 state 属性上的值,所以需要我们对 instance 实例作统一的代理。
function mountComponent(vnode, container) {
///
vnode.component = instance
// 对我们的实例作代理,使得this可以同时访问state和props
// 从代理的过程也可以看出如果组件上的data和props出现了同名属性,data上的属性优先级更高
const renderContext = new Proxy(instance, {
get(target, key) {
const { state, props } = target
if (state && key in state) {
return state[key]
} else if (key in props) {
return props[key]
} else {
console.log('属性不存在')
}
},
set(target, key, value) {
const { state, props } = target
if (state && key in state) {
state[key] = value
} else if (key in props) {
console.log('不能修改props上的数据')
} else {
console.log('属性不存在')
}
},
})
// 执行生命周期时this指向我们的代理对象,其他生命周期同理
created && created.call(renderContext)
///
}
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
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
vue3 新增了组件的 setup 选项,配合 vue 实现组合式 api,它只会在组件挂载的时候执行一次。
// setup函数可以返回一个函数,它将代替组件的render函数
const Comp = {
setup() {
return () => {
return { type: 'div', children: 'hello' }
}
},
}
// 也可以返回一个对象,这个对象包含的数据将暴露给模板使用
const Comp = {
setup() {
const count = ref(0)
return {
count,
}
},
// render函数中可以通过this来访问setup函数返回的数据
render() {
return { type: 'div', children: `count is ${this.count}` }
},
}
// setup函数可以接收两个参数,props和setupContext对象,setupContext对象上面储存了组件的相关接口
const Comp = {
props: {
foo: String,
},
setup(props, setupContext) {
// 访问props数据
console.log(props.foo)
// 组件的一系列接口
const { slots, emit, attrs, expose } = setupContext
return () => {
return { type: 'div', children: 'hello' }
}
},
}
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
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
setup 函数的实现。
// 组件编译结果
const Comp = {
data() {
return {}
},
props: {
foo: Number,
},
setup(props, setupContent) {
const count = ref(props.foo)
return {
count,
}
},
render() {
return { type: 'div', children: `count is ${this.count.value}` }
},
}
// 父组件vnode
const compVnode = {
type: Comp,
props: {
foo: 1,
},
}
function mountComponent(vnode, container) {
///
// 取出setup函数
const { setup } = componentOptions
const [props, attrs] = resolveProps(propsOptions, vnode.props)
const instance = {
state,
isMounted: false,
props: shallowReactive(props),
subTree: null,
}
// 定义setupContent对象,先处理attrs
const setupContent = {
attrs,
}
// 执行setup函数,拿到我们的返回结果,注意传入props的时候要转为只读的,防止setup函数内部修改props
const setupResult = setup(shallowReadonly(instance.props), setupContent)
// 定义setup的数据
let setupState = null
// 对setup的返回值和render函数做格式化
if (typeof setupResult === 'function') {
if (render) {
console.error('render函数将被setup函数的返回值覆盖')
}
render = setupResult
} else {
setupState = setupResult
}
vnode.component = instance
const renderContext = new Proxy(instance, {
get(target, key) {
const { state, props } = target
if (state && key in state) {
return state[key]
} else if (key in props) {
return props[key]
} else if (setupState && key in setupState) {
// 添加对setup返回对象的支持
return setupState[key]
} else {
console.log('属性不存在')
}
},
set(target, key, value) {
const { state, props } = target
if (state && key in state) {
state[key] = value
} else if (key in props) {
console.error('不能修改props上的数据')
} else if (setupState && key in setupState) {
// 添加对setup返回对象的支持
setupState[key] = value
} else {
console.log('属性不存在')
}
},
})
///
}
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
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
setup 函数中 emit 的实现
const Comp = {
setup(props, setupContent) {
// setup函数内部通过emit触发事件
const { emit } = setupContent
emit('click')
return {}
},
}
function handler() {
console.log(123)
}
// 使用组件的时候监听事件
// <Comp @click="handler"/>
const compVnode = {
type: Comp,
props: {
// 父组件监听事件
onClick: handler,
},
}
// mountComponent内部定义emit函数
function emit(event, ...payload) {
// 格式化事件名称 click --> onClick
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
// 从props中取出回调函数执行
const handler = instance.props[eventName]
if (handler) {
handler(...payload)
} else {
console.error('事件不存在')
}
}
// setupContent对象新增emit函数给setup函数使用
const setupContent = {
attrs,
emit,
}
// 注意之前格式化props的函数要增加on开头的属性处理
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
// on开头的属性默认为事件处理函数,直接挂到props下
if (key in options || key.startsWith('on')) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
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
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
setup 函数中 slots 的实现
// 组件内部定义插槽的位置时
// <template>
// <header>
// <slot name="header"></slot>
// </header>
// <div>
// <slot name="body"></slot>
// </div>
// <footer>
// <slot name="footer"></slot>
// </footer>
// </template>
// 上面的模板会编译为
const Comp = {
setup(props, setupContent) {
// setup内也可以访问slots
const { slots } = setupContent
console.log(slots)
return {}
},
render() {
// render函数可以通过this.$slots访问插槽
return {
type: 'div',
children: [
{
type: 'header',
children: [this.$slots.header()],
},
{
type: 'div',
children: [this.$slots.body()],
},
{
type: 'footer',
children: [this.$slots.footer()],
},
],
}
},
}
// 父组件调用组件使用插槽时
// <Comp>
// <template #header>
// <h1>我是header</h1>
// </template>
// <template #body>
// <section>我是body</section>
// </template>
// <template #footer>
// <p>我是footer</p>
// </template>
// </Comp>
// 上面的父组件模板会被编译为
const compVnode = {
type: Comp,
children: {
header() {
return { type: 'h1', children: '标题' }
},
body() {
return { type: 'section', children: '内容' }
},
footer() {
return { type: 'p', children: '注脚' }
},
},
}
function mountComponent(vnode, container) {
///
// 直接将children定义为插槽
const slots = vnode.children || {}
const instance = {
state,
isMounted: false,
props: shallowReactive(props),
subTree: null,
// 挂载到实例上,让this能够访问到
slots,
}
///
const setupContent = {
attrs,
emit,
// 添加到setupContent,让setup函数能访问到
slots,
}
const renderContext = new Proxy(instance, {
get(target, key) {
const { state, props, slots } = target
// 如果访问的事this.$slots,就返回slots
if (key === '$slots') return slots
if (state && key in state) {
return state[key]
} else if (key in props) {
return props[key]
} else if (setupState && key in setupState) {
return setupState[key]
} else {
console.log('属性不存在')
}
},
set(target, key, value) {
const { state, props } = target
if (state && key in state) {
state[key] = value
} else if (key in props) {
console.error('不能修改props上的数据')
} else if (setupState && key in setupState) {
setupState[key] = value
} else {
console.log('属性不存在')
}
},
})
///
}
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
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
setup 函数中 注册生命周期
// 在vue3中使用组合式api注册生命周期
import { onMounted } from 'vue'
const Mycomponent = {
setup() {
onMounted(() => {
console.log(1)
})
// 可以注册多个相同的生命周期
onMounted(() => {
console.log(2)
})
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
生命周期的注册函数是相同的,但是生命周期执行的函数需要注册到当前的组件上面,所以需要一个当前实例来表示当前正在执行注册的组件
// 全局注册当前实例和修改当前实例的函数
let currentInstance = null
function setCurrentInstance(instance) {
currentInstance = instance
}
function mountComponent(vnode, container) {
///
const instance = {
state,
isMounted: false,
props: shallowReactive(props),
subTree: null,
slots,
// 同一个生命周期函数可以注册多个,所以用数组来储存
mounted: [],
}
const setupContent = {
attrs,
emit,
slots,
}
// 调用setup函数之前设置当前实例
setCurrentInstance(instance)
const setupResult = setup(shallowReadonly(instance.props), setupContent)
// 调用setup函数之后清空当前实例
setCurrentInstance(null)
///
effect(
() => {
const subTree = render.call(renderContext, renderContext)
if (!instance.isMounted) {
///
// 取出当前实例中的mounted数组,执行onMounted函数注册的生命周期
instance.mounted && instance.mounted.forEach((hook) => hook.call(renderContext))
} else {
///
}
instance.subTree = subTree
},
{
scheduler: queueJob,
}
)
///
}
// 实现onMounted函数
function onMounted(fn) {
// 当前实例存在,则找到mounted数组,存入函数
if (currentInstance) {
currentInstance.mounted.push(fn)
} else {
// 当前实例不存在,表示在setup外调用了onMounted,直接报错
console.log('onMounted只能在setup函数内调用')
}
}
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
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
其他的生命周期注册流程和 mounted 相同。