返回

专栏第十篇-组件系统

在前面的章节中,我们一直在说普通的节点渲染,但是很少提及组件相关的内容,这节中我们就把“组件”好好的说道说道。

一、工作中遇到的困惑

组件是Vue框架的核心特性。组件系统提供了构建可复用、可维护且性能优化的组件的能力。

使用组件功能也很简单,只需要注册组件,然后就可以在模版中使用了。

1
2
3
4
5
6
7
8
// 1. 第一步先注册组件
Vue.component('hello',{
template:`<div>Hello World</div>`
})
// 2. 第二步在模版中使用组件
<template>
<hello />
</template>

但是我们工作中总会莫名其妙的遇到组件注册没有生效的问题。

这里列举几个常见的错误。

1.1 组件未在Vue实例化之前注册

有时候我们不清楚组件注册的原理,会下意识的认为无论在哪里注册组件,都会成功注册组件并在模版中可以正确使用。

所以有时候我们注册的时机就会存在问题。比如下面这种情况:

1
2
3
4
5
6
7
8
9
10
11
// 先渲染渲染Test组件
new Vue(Test)
// 然后再注册组件
Vue.component('hello',{
template:`<div>Hello World</div>`
})
// 在模版中使用组件
// Test.vue
<template>
<hello />
</template>

上面先实例化Vue渲染Test组件然后再注册组件,此时浏览器无法渲染出Test组件内容。

1.2 注册组件未遵循命名规范

在Vue中,推荐使用驼峰命名法作为组件名。如果我们在注册组件时使用了带有下划线和短横线的命名方式,Vue将无法正确识别该组件。

1
2
3
4
5
6
7
8
9
10
11
12
// 先注册组件(使用短横线命名组件名)
Vue.component('hello-c',{
template:`<div>Hello World</div>`
})
// 再渲染Test组件
new Vue(Test)

// 在模版中使用组件(使用驼峰法使用组件)
// Test.vue
<template>
<helloC />
</template>

如上,在模版定义时使用短横线命名组件,那么在模版中就无法使用驼峰写法,反之是可以的。

1.3 组件名称冲突

有时当系统足够复杂、组件很多的情况下,编写的组件名重复是很常见的。

当不同的组件使用相同的名称时,可能会导致全局组件注册失败。Vue会优先使用最后注册的组件,覆盖之前的同名组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 先注册组件
Vue.component('HelloC',{
template:`<div>Hello World1</div>`
})
// 注册同名组件
Vue.component('HelloC',{
template:`<div>Hello World2</div>`
})
// 再渲染Test组件
new Vue(Test)

// 在模版中使用组件
// Test.vue
<template>
<hello-c />
</template>

此时浏览器中会渲染第二次组件定义的模版内容。

上面的这几种情况都是框架给我们规定的,那么为什么会这样呢?我们带着这几个疑问再去看Vue组件的源码。

二、组件注册的方式

组件注册的方式一般有2种:全局注册、局部注册

2.1 全局注册API:Vue.component

专栏第三篇中,我们知道在Vue被引入时调用了initGlobalAPI将component属性挂在到Vue构造函数上。

那么Vue.component内部究竟做了什么操作呢?

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
const ASSET_TYPES = ['component', 'directive', 'filter'];
export function initAssetRegisters(Vue){
ASSET_TYPES.forEach(type => {
Vue[type] = function(
id,
definition
){
if (!definition) {
return this.options[type + 's'][id]
}else{
/* istanbul ignore if */
if (__DEV__ && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
// 优先取name 如果没有就使用id
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}

如果你之前看过专栏第五篇,对于上面这段代码肯定一眼就看出来了。

在Vue被引入时,会在构造函数的options.components挂载一些内部组件,如下图:

而使用Vue.component方法实际上就是在选项的components中挂载一些其他的组件。

2.2 局部注册

如果你想注册一个内部组件,只想在某个模块下使用这个组件,而不是所有模块都可以使用,你可以在使用的模块内部引入这个组件,并在选项上添加components选项注册只在当前模块下使用的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Test.vue -->
<template>
<!-- 使用这个组件 -->
<hello />
</template>

<script>
import Hello from "./Hello.vue"
export default {
// 在当前模块下注册局部组件
components:{
Hello
}
}
</script>

三、组件渲染的核心流程

经过前面的学习,我们知道了Vue是一个基于虚拟DOM的前端框架。

所以渲染分为2步:1. 获取需要渲染的虚拟DOM(vm._render()) 2. 将虚拟DOM对应的内容渲染到页面上(vm._update())

3.1 获取组件的虚拟DOM

3.1.1 模版编译

我们知道,我们在.vue文件内编写的内容最终会通过webpack+vue-loader编译成为一个对象,其中template的内容会被编译成一个render函数。如下,在模版中编写了一个组件标签:

1
2
3
4
5
<!-- Test.vue -->
<template>
<!-- 使用这个组件 -->
<hello />
</template>

会编译成下面这种render函数。

_c函数是createElement的缩写形式:

  1. 如果是普通html标签,createElement的第一个参数是对应的标签字符串如:div
  2. 如果是组件标签,createElement的第一个参数同样是字符串,不过字符串的内容是在模版中写的标签名称如:hello

3.1.2 createElement创建vnode

当tag传入为字符串并且能够根据tag在组件实例的$options.components上获取到内容时,则调用createComponent生成组件虚拟DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function _createElement(
tag
){
let vnode,Ctor
if(typeof tag === 'string'){
// 如果是普通标签,如:'div'
if (config.isReservedTag(tag)) {
}else if(
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
){
vnode = createComponent(Ctor, data, context, children, tag)
}
}
}

3.1.2.1 知识回顾-组件实例选项

前面我们通过在实例的组件选项上获取,如果获取到了,说明组件注册成功,那么注册的组件为什么可以在vm.$options.components上获取到呢?

专栏第五篇中,我们知道除了根Vue页面外,每一个Vue页面都存在一个组件构造函数和一个组件构造函数实例

这个组件构造函数是基于Vue构造函数创建的,上面的选项同样合并了Vue构造函数的选项。

在进行渲染时,如果遇到组件类型的虚拟DOM,会使用组件构造函数实例化,并在实例化时合并传入的选项以及对应构造函数的选项。

所以 实例上的组件选项 = 构造函数传入的组件选项 + 构造函数上的选项 + Vue构造函数的选项

所以通过下面2种方式注册的组件,可以通过vm.$options.components获取到:

  1. 全局注册的组件,会挂载到Vue构造函数的选项上。
  2. 局部注册的组件,会挂载到组件构造函数的选项上。

3.1.2.2 resolveAsset获取具体的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function resolveAsset(
options: Record<string, any>,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (__DEV__ && warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id)
}
return res
}

resolveAsset 函数提供了一种灵活的方式来解析组件中的资源,支持多种命名约定,并且在资源未找到时提供警告。

可以看出来并不一定要属性名完全一致才可以匹配成功。

  1. 第一步先根据传入的属性名在资源对象本身的属性中查找有没有完全一致的。如果存在,直接返回对应的资源;如果没有,继续向下执行。
  2. 第二步将传入的属性名由中横线转化为小驼峰(如果有的话)再在资源对象本身的属性中进行匹配。如果存在,直接返回对应的资源;如果没有,继续向下执行。
  3. 第三步将传入的属性名首字母大写再在资源对象本身的属性中进行匹配。如果存在,直接返回对应的资源;如果没有,继续向下执行。
  4. 第四步在对象的原型上查找对应的属性。如果存在,直接返回对应的资源。否则,报错查找不到对应的资源。

3.1.2.3 createComponent创建组件的虚拟DOM

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
export function createComponent(
Ctor: typeof Component | Function | ComponentOptions | void,
data: VNodeData | undefined,
context: Component,
children?: Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 创建组件虚拟节点一定要有组件的内容,如果没有组件的内容,直接返回,不进行创建。
if (isUndef(Ctor)) {
return
}
// 获取创建组件构造函数的基类构造函数:Vue
const baseCtor = context.$options._base

// 判断组件内容是对象,调用extend方法生成组件构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor as typeof Component)
}

// 组件构造函数一定是一个构造函数,如果组件构造函数不是一个函数,直接报错非法的组件定义
if (typeof Ctor !== 'function') {
if (__DEV__) {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}

// 异步组件
let asyncFactory
// @ts-expect-error
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}

// 设置data
data = data || {}

// 防止在构造函数实力化后应用全局混合
resolveConstructorOptions(Ctor as typeof Component)

// 转化组件的model
if (isDef(data.model)) {
// @ts-expect-error
transformModel(Ctor.options, data)
}

// props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)

// 函数组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(
Ctor as typeof Component,
propsData,
data,
context,
children
)
}

// 监听器
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn

// @ts-expect-error
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot

// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}

// install component management hooks onto the placeholder node
installComponentHooks(data)

// return a placeholder vnode
// @ts-expect-error
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
// @ts-expect-error
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
// @ts-expect-error
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)

return vnode
}

这个函数主要有下面几个核心功能点:

  1. 创建组件构造函数

使用Vue.extend创建组件构造函数Ctor。

  1. 处理prop、model、异步组件、函数组件等。

  2. 调用componentVNodeHooks方法在data上定义一些hook,在后续渲染时调用。

  3. 将Ctor等属性放在VNode的componentOptions属性上。

下图是普通虚拟节点和组件虚拟节点常用属性的差异对比。

3.2 渲染组件VNode

3.2.1 回顾下普通虚拟节点创建真实DOM的流程


本站总访问量