# props 与组件的被动更新
在虚拟DOM层面,组件的props与普通的HTML标签的属性相差不大.假设我们有以下模板:
<MyComponent title="a Big Title" :other="val"></MyComponent>
1
这段模板对应的虚拟DOM是:
const vnode = {
type: MyComponent,
props: {
title: 'a Big Title',
other: this.val
}
}
1
2
3
4
5
6
7
2
3
4
5
6
7
在编写组件的时候,我们需要显式指定组件会接受到哪些props数据.
const MyComponent = {
name: 'MyComponent',
props: {
title: String,
},
render() {
return {
type: 'div',
children: this.title //访问props数据
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
对于一个组件来说,有两部分props内容需要我们担心
- 为组件传递的props数据,即组件的vnode.props对象
- 组件选项对象中定义的props选项,即MyComponent.props对象
我们需要结合这两个选项解析出组件在渲染时要用到的props数据.
function mountComponent(vnode, container,anchor) {
const componentOptions = vnode.type
const { render,props:propsOptions,data } = componentOptions
const state = reactive(data())
//调用resloveProps解析props与attrs数据
const [props,attrs] = resloveProps(propsOptions,vnode.props)
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
}
vnode.component = instance
//省略部分..
}
function resloveProps(options,propsData) {
const props = {}
const attrs = {}
for(const key in propsData) {
if(key in options) {
//如果传递的props数据在组件自身的props选项中有定义,则视为合法的props
props[key] = propsData[key]
} else {
//否则视为attrs
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
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
这里我们需要注意2点
- 在Vue3.js中,没有定义在MyComponent.props选项中的props数据,会被收集到attrs对象中
- 上述实现没有包含默认值和类型校验等内容的处理,但实际上也是围绕MyComponent.props和vnode.props产展开的,实现并不复杂.
处理完props后,我们来讨论props数据变化的问题.props本质上是父组件的数据,当props数据发生变化的时候,会触发父组件重新渲染,也就是父组件的自更新.
在更新过程中,渲染器发现父组件的subTree包含组件类型的虚拟节点,所以会调用patchComponent函数完成子组件的更新.
funtion patch(n1,n2,container,anchor) {
if(n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if(typeof type === 'string') {
//
} else if (type === Text) {
//
} else if (type === Fragment) {
//
} else if (typeof type === 'object') {
if(!n1) {
mountComponent(n2,container,anchor)
} else {
//更新组件
patchComponent(n1,n2,container,anchor)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
我们吧父组件自更新引起的子组件更新叫做子组件的被动更新.当子组件发生被动更新时,我们需要做
- 检测子组件是否真的需要更新,因为子组件的props可能是不变的
- 如果需要更新,则更新子组件的props,slots等内容
patchComponent函数的具体实现如下
function patchComponent(n1,n2,anchor) {
// 获取组件实例n1.component 同时让新的虚拟节点n2.component也指向组件实例
const instance = (n2.component = n1.component)
const { props } = instance// 获取当前组件的props数据
if(hasPropsChanged(n1.props,n2.props)) {
//重新获取props数据
const [nextProps] = resloveProps(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]
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
以上就是组件被动更新的最小实现,有两点需要注意.
- 需要将组件实例添加到新的组件vnode对象上,否则下次更新的时将从无法获取组件实例
- instance.props 对象本身是浅响应的,因此在更新组件props数据的时,只需要设置instance.props对象下的属性值即可触发组件重新渲染.
由于props数据与组件自身的状态数据都需要暴露在渲染函数中,并使得渲染函数能通过this访问,我们需要封装一个上下文对象.
function mountComponent(vnode, container,anchor) {
//省略部分..
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
}
vnode.component = instance
//创建上下文对象,本质上是对组件实例的代理
const renderContext = new Proxy(instance,{
get(t,k,r) {
//获取组件自身状态与props数据
const { props,state } = t
//先尝试读取自身数据
if(state && k in state) {
return state[k]
} else if(k in props) { //尝试从props中读取
return props[k]
} else {
console.log('Not Found')
}
},
set(t,k,v,r) {
const { props,state } = t
if(state && k in state) {
state[k] = v
} else if(k in props) {
props[k] = v
} else [
console.log('Not Found')
]
}
})
//生命周期调用的时候要绑定渲染上下文对象
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
36
37
38
39
40
41
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
在上述代码中,我们创建了一个渲染上下文对象,每当渲染函数或者生命周期钩子通过this来读取数据的时候,会逐级读取.
除此之外,完整的组件还包含methods,computed等选项中定义的数据和方法,这些内容都应该在渲染上下文对象中暴露出来.