结合Vue3-demo项目分析源码(三)

结合Vue3-demo项目分析源码(二),创建完App实例,开始创建组件。接上篇mountComponent()方法,调用了createComponentInstance方法创建了一个初始化状态的组件实例instance,接下来就根据组件的定义调用方法setupComponent()设置此组件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
const mountComponent: MountComponentFn = (
...
) => {
//创建组件实例
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(...))
...
//设置组件实例
setupComponent(instance)
...
//处理setup方法
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense,isSVG, optimized)
...
}

分析

setupComponent()

创建组件实例

查看文件vue-next/packages/runtime-core/src/component.ts第520行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR

const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
//初始化属性
initProps(instance, props, isStateful, isSSR)
//初始化插槽
initSlots(instance, children)
//处理setup函数
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}

initProps()

初始化属性

查看文件vue-next/packages/runtime-core/src/componentProps.ts第129行

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
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: number, // result of bitwise flag comparison
isSSR = false
) {
const props: Data = {}
const attrs: Data = {}
def(attrs, InternalObjectKey, 1)
//设置属性
setFullProps(instance, rawProps, props, attrs)
// validation
if (__DEV__) {
validateProps(props, instance)
}

if (isStateful) { //普通组件
// stateful
//非服务端渲染时,默认将属性变成浅层响应式对象
instance.props = isSSR ? props : shallowReactive(props)
} else { //Element类型和函数组件
//如果组件没有提供属性,则把它全部attrs当作他的属性
if (!instance.type.props) {
// functional w/ optional props, props === attrs
instance.props = attrs
} else {
// functional w/ declared props
instance.props = props
}
}
instance.attrs = attrs
}

理解普通组件和其他组件(Element类型和函数组件)属性设置

方法initProps()中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (isStateful) { //普通组件
// stateful
//非服务端渲染时,默认将属性变成浅层响应式对象
instance.props = isSSR ? props : shallowReactive(props)
} else { //Element类型和函数组件
//如果组件没有提供属性,则把它全部attrs当作他的属性
if (!instance.type.props) {
// functional w/ optional props, props === attrs
instance.props = attrs
} else {
// functional w/ declared props
instance.props = props
}
}
  1. 普通组件

app即为普通组件,不是函数式也不是Element类型。故默认将它设置成浅层响应式对象shallowReactive

浅层响应式对象:只为某个对象的私有(第一层)属性创建浅层的响应式代理,不会对“属性的属性”做深层次、递归地响应式代理,而只是保留原样

App.vue添加一个属性user,设置其被点击就更改user.name字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
{{ msg }}
{{ testWord }}
<div @click="user.name = 'aaa'">我是:{{ user && user.name}}</div>
</div>
</template>
<script>
export default {
name: "app",
props: ["msg", "testWord","user"],
created(){
console.log(this)
console.log("this对象如上")
}
};
</script>

并且在main.js文件将其改为响应式对象

1
2
3
4
5
6
const app = createApp(App, { 
msg: "hello",
msg1: "vue",
"test-word": "我是用 “-” 符号传递的属性",
user: reactive({name: "testname"})
});//渲染App组件,传递msg参数

则通过点击,该属性的name变成了aaa。但此对象仅其自有属性name为响应式,而其属性仍为非响应式,即仅有第一层

  1. Element类型及函数类型

并且在main.js文件新增函数类型组件,并为其添加属性msg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const AppWrapper = (props) => {//函数式组件
return h("div",[h(App,props)]);
};
AppWrapper.props = {//给函数式组件设置属性
msg: String,
}

const app = createApp(AppWrapper, {
msg: "hello",
msg1: "vue",
"test-word": "我是用 “-” 符号传递的属性",
user: reactive({name: "testname"})
});//渲染App组件,传递msg参数
app.mount("#app")//渲染app,将其挂载到id=app的节点上

则其定义的属性attrmsg被作为prop,其他属性则被当作attrs传递

attrs

setFullProps()

设置属性

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
function setFullProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
props: Data,
attrs: Data
) {
const [options, needCastKeys] = instance.propsOptions
if (rawProps) {
for (const key in rawProps) {
const value = rawProps[key]
// key, ref are reserved and never passed down
if (isReservedProp(key)) {
continue
}
// prop option names are camelized during normalization, so to support kebab -> camel conversion here we need to camelize the key.
let camelKey
//如果有给组件传递的参数,并在当前组件中定义了该参数。就把传递的参数传给当前组件
if (options && hasOwn(options, (camelKey = camelize(key)))) {
props[camelKey] = value
} else if (!isEmitListener(instance.emitsOptions, key)) {
//否则把传递的属性传给attrs
attrs[key] = value
}
}
}
//传递的属性中是否含有 - 符号定义的属性,有的话就获取该属性
if (needCastKeys) {
const rawCurrentProps = toRaw(props)
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(
options!,
rawCurrentProps,
key,
rawCurrentProps[key],
instance
)
}
}
}

代码理解

  1. 理解属性传递部分
1
2
3
4
5
6
if (options && hasOwn(options, (camelKey = camelize(key)))) {
props[camelKey] = value
} else if (!isEmitListener(instance.emitsOptions, key)) {
//否则把传递的属性传给attrs
attrs[key] = value
}

即若给app添加两个属性

1
const app = createApp(App, { msg: "hello", msg1: "vue" });

此时,App.vue<template>没有定义根元素,仅为<template></template>。故Vue自动帮它加了一个Fragment片段,故无法显示

当给App.vue加上根节点<div></div>

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
name: "app",
props: ["msg"],
created(){
console.log(this)
console.log("this对象如上")
}
};
</script>

由于在<script>仅给组件传递了参数msg,故msg1被当作属性给attrs

29qxmj.png

  1. 理解“-”符号定义的属性部分

含有“-”符号定义的属性即在App.vue中定义的类似于testWord含有大写的驼峰命名法定义的属性。在被挂载到app上时,会被命名为test-word。故需要对类似的属性进行处理

initSlots()

初始化插槽

查看文件vue-next/packages/runtime-core/src/componentSlots.ts第109行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
//创建节点时传递的children为对象时
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
instance.slots = children as InternalSlots
// make compiler marker non-enumerable
def(children as InternalSlots, '_', type)
} else {
normalizeObjectSlots(children as RawSlots, (instance.slots = {}))
}
} else {//当传递的children为数组时,直接在slots里暴露一下default方法
instance.slots = {}
if (children) {
normalizeVNodeSlots(instance, children)
}
}
def(instance.slots, InternalObjectKey, 1)
}

有状态组件vs无状态组件

  • 有状态组件:需要维护内部状态的组件。通常被称为容器container,通常会包含若干无状态组件。如App.vue此类SFC组件,可以利用datasetup方法定义自己的状态

  • 无状态组件:不需要维护状态的组件,自己没有数据,要么是纯渲染的html内容,要么数据都是来自上层结构。如上面的函数组件AppWrapper

查看文件vue-next/packages/shared/src/shapeFlags.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const enum ShapeFlags {
//无状态组件
ELEMENT = 1, //dom元素类型,如div、span等普通dom标签
FUNCTIONAL_COMPONENT = 1 << 1, //函数式类型,如AppWrapper
//有状态组件
STATEFUL_COMPONENT = 1 << 2, //静态类型,如svg标签
TEXT_CHILDREN = 1 << 3, //文本节点类型
ARRAY_CHILDREN = 1 << 4, //创建节点时传递的子节点是数组时
SLOTS_CHILDREN = 1 << 5, //创建节点时传递的子节点是对象时
TELEPORT = 1 << 6, //TELEPORT组件
SUSPENSE = 1 << 7, //SUSPENSE组件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, //keep-alive组件的子组件
COMPONENT_KEPT_ALIVE = 1 << 9, //keep-alive组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

setupStatefulComponent()

设置有状态组件

查看文件vue-next/packages/runtime-core/src/component.ts第538行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
...
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy also mark it raw so it's never observed
//创建当前组件实例的代理对象,即demo项目打印出的this对象
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
...
// 2. call setup()
const { setup } = Component
...
}

当通过Proxy访问instance属性时,需要经过方法PublicInstanceProxyHandlers

PublicInstanceProxyHandlers

查看文件vue-next/packages/runtime-core/src/componentPublicInstance.ts第243行

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
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
...
//当使用组件用this访问不带’$‘的属性时
if (key[0] !== '$') {
//查看缓存中是否存在
const n = accessCache![key]
//缓存中存在,按顺序获取
if (n !== undefined) {
switch (n) {
//1.从setupState中获取
case AccessTypes.SETUP:
return setupState[key]
//2.从data中获取
case AccessTypes.DATA:
return data[key]
//3.从组件上下文对象中获取
case AccessTypes.CONTEXT:
return ctx[key]
//4.从组件属性里获取
case AccessTypes.PROPS:
return props![key]
// default: just fallthrough
}
//缓存中不存在,则按以下顺序获取
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
//1.从setupState中获取,并存入缓存
accessCache![key] = AccessTypes.SETUP
return setupState[key]
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
//2.从data中获取,并存入缓存
accessCache![key] = AccessTypes.DATA
return data[key]
} else if (
// only cache other properties when instance has declared (thus stable)
// props
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
//3.从组件属性props中获取,并存入缓存
accessCache![key] = AccessTypes.PROPS
return props![key]
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
//4.最后从组件上下文对象中获取,并存入缓存
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (!__FEATURE_OPTIONS_API__ || !isInBeforeCreate) {
accessCache![key] = AccessTypes.OTHER
}
}


const publicGetter = publicPropertiesMap[key]
let cssModule, globalProperties
// public $xxx properties
//获取带‘$‘的公共属性,如$el、$data、$props等
if (publicGetter) {
if (key === '$attrs') {
track(instance, TrackOpTypes.GET, key)
__DEV__ && markAttrsAccessed()
}
return publicGetter(instance)
} else if (
// css module (injected by vue-loader)
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])
) {
return cssModule
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// user may set custom properties to `this` that start with `$`
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (
//前面方法都找不到则到app全局对象中找
// global properties
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))
) {
return globalProperties[key]
} else if (
__DEV__ &&
currentRenderingInstance &&
(!isString(key) ||
// #1091 avoid internal isRef/isVNode checks on component instance leading
// to infinite warning loop
key.indexOf('__v') !== 0)
) {
...
}
},
...
}

其中,Vue3新增的内容为:

1
2
3
4
5
6
7
else if (
//前面方法都找不到则到app全局对象中找
// global properties
((globalProperties = appContext.config.globalProperties), hasOwn(globalProperties, key))
) {
return globalProperties[key]
}

Vue3不再以一个Vue对象的形式创建组件,而使用组合式API,故不能在Vue原形上绑定一些全局的属性,但Vue3在app对象上暴露了globalProperties供使用。

在定义全局api时,建议使用$来定义,如

1
2
3
4
5
6
// main.js文件添加一个全局的 $showSuccuessDialog 方法
app.config.globalProperties.$showSuccuessDialog = function (msg) {
alert(msg);
};
// App.vue文件中调用全局的方法
this.$showSuccuessDialog("我是全局的 showSuccuessDialog 方法");

由方法PublicInstanceProxyHandlers得知,当获取$开头的属性时,可以绕过setStatedataprop等前置条件,可以更快的取到全局属性,从而提升性能效率。

附录-Vue3流程图

vue流程图

  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2020-2024 Aweso Lynn
  • PV: UV: