挂载组件中有一个创建渲染上下文代理的流程,为什么要需要代理?

首先是创建渲染上下文代理的流程,它主要对 instance.ctx 做了代理。
在 Vue.js 2.x 中,也有类似的数据代理逻辑,比如 props 求值后的数据,实际上存储在 this._props 上,而 data 中定义的数据存储在 this._data 上。

在初始化组件的时候,data 中定义的 msg 在组件内部是存储在 this._data 上的,而模板渲染的时候访问 this.msg,实际上访问的是 this._data.msg,这是因为 Vue.js 2.x 在初始化 data 的时候,做了一层 proxy 代理。

到了 Vue.js 3.0,为了方便维护,我们把组件中不同状态的数据存储到不同的属性中,比如存储到 setupState、ctx、data、props 中。我们在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以我们也要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupState、ctx、data、props 中的数据的访问和修改。

分析 proxy 的几个方法: get、set 和 has。

get方法

访问渲染上下文 instance.ctx 中的属性,就会进入get属性。

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
const PublicInstanceProxyHandlers = {
get ({ _: instance }, key) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance
if (key[0] !== '$') {
// setupState / data / props / ctx
// 渲染代理的属性访问缓存中
const n = accessCache[key]
if (n !== undefined) {
// 从缓存中取
switch (n) {
case 0: /* SETUP */
return setupState[key]
case 1 :/* DATA */
return data[key]
case 3 :/* CONTEXT */
return ctx[key]
case 2: /* PROPS */
return props[key]
}
}
else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache[key] = 0
// 从 setupState 中取数据
return setupState[key]
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache[key] = 1
// 从 data 中取数据
return data[key]
}
else if (
type.props &&
hasOwn(normalizePropsOptions(type.props)[0], key)) {
accessCache[key] = 2
// 从 props 中取数据
return props[key]
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 3
// 从 ctx 中取数据
return ctx[key]
}
else {
// 都取不到
accessCache[key] = 4
}
}
const publicGetter = publicPropertiesMap[key]
let cssModule, globalProperties
// 公开的 $xxx 属性或方法
if (publicGetter) {
return publicGetter(instance)
}
else if (
// css 模块,通过 vue-loader 编译的时候注入
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])) {
return cssModule
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// 用户自定义的属性,也用 `$` 开头
accessCache[key] = 3
return ctx[key]
}
else if (
// 全局定义的属性
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))) {
return globalProperties[key]
}
else if ((process.env.NODE_ENV !== 'production') &&
currentRenderingInstance && key.indexOf('__v') !== 0) {
if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
// 如果在 data 中定义的数据以 $ 开头,会报警告,因为 $ 是保留字符,不会做代理
warn(`Property ${JSON.stringify(key)} must be accessed via $data because it starts with a reserved ` +
`character and is not proxied on the render context.`)
}
else {
// 在模板中使用的变量如果没有定义,报警告
warn(`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`)
}
}
}
}

1、函数首先判断 key 不以 $ 开头的情况,这部分数据可能是 setupState、data、props、ctx 中的一种,其中 data、props 我们已经很熟悉了;setupState 就是 setup 函数返回的数据。

依次判断 setupState、data、props、ctx 中是否包含这个key, 如果包含就返回对应值.判断顺序决定了取对应值的优先级。

2、再回到 get 函数中,我们可以看到这里定义了 accessCache 作为渲染代理的属性访问缓存,它具体是干什么的呢?
组件在渲染时会经常访问数据进而触发 get 函数,这其中最昂贵的部分就是多次调用 hasOwn 去判断 key 在不在某个类型的数据中,但是在普通对象上执行简单的属性访问相对要快得多。所以在第一次获取 key 对应的数据后,我们利用 accessCache[key] 去缓存数据,下一次再次根据 key 查找数据,我们就可以直接通过 accessCache[key] 获取对应的值,就不需要依次调用 hasOwn 去判断了。

3、key 以 $ 开头。

  • 首先判断是不是 Vue.js 内部公开的 $xxx 属性或方法(比如 $parent)
  • 然后判断是不是 vue-loader 编译注入的 css 模块内部的 key;
  • 接着判断是不是用户自定义以 $ 开头的 key;
  • 最后判断是不是全局属性。
  • 如果都不满足,就剩两种情况了,即在非生产环境下就会报两种类型的警告,第一种是在 data 中定义的数据以 $ 开头的警告,因为 $ 是保留字符,不会做代理;第二种是在模板中使用的变量没有定义的警告。
set方法

修改 instance.ctx 渲染上下文中的属性的时候,就会进入 set 函数。

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
const PublicInstanceProxyHandlers = {
set ({ _: instance }, key, value) {
const { data, setupState, ctx } = instance
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
// 给 setupState 赋值
setupState[key] = value
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
// 给 data 赋值
data[key] = value
}
else if (key in instance.props) {
// 不能直接给 props 赋值
(process.env.NODE_ENV !== 'production') &&
warn(`Attempting to mutate prop "${key}". Props are readonly.`, instance)
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
// 不能给 Vue 内部以 $ 开头的保留属性赋值
(process.env.NODE_ENV !== 'production') &&
warn(`Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`, instance)
return false
}
else {
// 用户自定义数据赋值
ctx[key] = value
}
return true
}
}

函数主要做的事情就是对渲染上下文 instance.ctx 中的属性赋值,它实际上是代理到对应的数据类型中去完成赋值操作的。这里仍然要注意顺序问题,和 get 一样,优先判断 setupState,然后是 data,接着是 props。

如果是用户自定义的数据,比如在 created 生命周期内定义的数据,它仅用于组件上下文的共享,会将数据保留到 ctx 中。

has 代理过程

判断属性是否存在于 instance.ctx 渲染上下文中时,就会进入 has 函数。
举个🌰:当执行 created 钩子函数中的 ‘msg’ in this 时,就会触发 has 函数。
依次判断 key 是否存在于 accessCache、data、setupState、props 、用户自定义数据、公开属性以及全局属性中,然后返回结果。

源码中组件挂载时有一步判断处理 setup 函数的流程,都做了什么?

1、创建 setup 函数上下文

setup中参数>1 ,则创建上下文。

1
2
3
4
5
6
7
8
9
10
11
// 判断处理 setup 函数
const { setup } = Component
if (setup) {
// 如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 执行 setup 函数获取结果
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
// 处理 setup 执行结果
handleSetupResult(instance, setupResult)
}

举个🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<p>{{ msg }}</p>
<button @click="onClick">Toggle</button>
</template>
<script>
export default {
props: {
msg: String
},
setup (props, { emit }) {
function onClick () {
emit('toggle')
}
return {
onClick
}
}
}
</script>

我们在父组件引用这个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<HelloWorld @toggle="toggle" :msg="msg"></HelloWorld>
</template>
<script>
import { ref } from 'vue'
import HelloWorld from "./components/HelloWorld";
export default {
components: { HelloWorld },
setup () {
const msg = ref('Hello World')
function toggle () {
msg.value = msg.value === 'Hello World' ? 'Hello Vue' : 'Hello World'
}
return {
toggle,
msg
}
}
}
</script>

HelloWorld 子组件的 setup 函数接收两个参数,第一个参数 props 对应父组件传入的 props 数据,第二个参数 emit 是一个对象,实际上就是 setupContext。
下面来看一下createSetupContext 函数来创建 setupContext

1
2
3
4
5
6
7
function createSetupContext (instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit
}
}

setupContext 让我们在 setup 函数内部可以获取到组件的属性、插槽以及派发事件的方法 emit。

2、执行 setup 函数并获取结果
1
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
3、handleSetupResult 函数来处理结果
1
handleSetupResult(instance, setupResult)

函数处理逻辑:当 setupResult 是一个对象的时候,我们把它变成了响应式并赋值给 instance.setupState,这样在模板渲染的时候,依据前面的代理规则,instance.ctx 就可以从 instance.setupState 上获取到对应的数据,这就在 setup 函数与模板渲染间建立了联系。

setup 不仅仅支持返回一个对象,也可以返回一个函数作为组件的渲染函数


总结:大功告成✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️

参考链接: