vue源码分析(十五) 选项合并之规范化
# 1. 概述
前面几章我们分析了 Vue
源码的整体结构、原型属性、静态属性、平台化以及全局配置,接下来的我们 Vue
实例化作为入口来分析,Vue
源码的执行的流程和细节处理。
本章我们将会分析 Vue
实例化中的选项处理逻辑,在分析之前,我们先看一个案例,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue 源码分析</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// Vue.config.devtools = true
// Vue.config.performance = true
new Vue({
el: '#app',
template: `
<div> {{ name }} </div>
`,
data: {
name: 'robin'
}
})
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 2. _init
我们前面分析过了 new Vue
实质是执行了 Vue.prototype._init
方法,我们回到 _init
方法,以当前案例逐句分析,首先我们看一下下面这段代码,如下:
源码目录:src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
/* 省略... */
}
}
2
3
4
5
6
7
8
首先声明变量 vm
赋值为 this
,我们知道此时的_init
方法是通过 vm._init(options)
调用的,所以此处的 this
是 Vue
实例。
接下来是在 Vue
实例上添加内部属性 _uid
,作用是当前实例的唯一标示,每次实例化一个 Vue
实例,_uid
就会加一。
我们继续往下看,如下:
源码目录:src/core/instance/init.js
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
/* 省略... */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
2
3
4
5
6
7
8
9
10
11
12
13
这段代码的主要作用是浏览器性能监控。
说明:
关于 config.performance
可以参考官网 (opens new window)。
关于浏览器性能检测 performance API
可以查看 MDN (opens new window) 或 这里 (opens new window)。
我们继续往下看,代码如下:
源码目录:src/core/instance/init.js
vm._isVue = true
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
2
3
4
5
6
7
8
9
10
11
12
13
首先给 Vue
实例添加一个 isVue
属性,用来标识一个对象是 Vue
实例,作用是可以避免该对象被响应系统观测。接下来判断配置项 options
存在并且 options._isComponent
属性为 true
才会执行 if
语句,否则执行 else
语句。
这里 initInternalComponent
方法的作用是内部组件实例化,因为 Vue
动态合并策略非常慢,并且内部组件的选项都不需要特殊处理。
我们当前案例会执行 else
语句,else
语句中通过调用 mergeOptions
将 Vue
实例化时传入的 options
和 Vue
自身的 options
进行合并保存到 vm.$options
。关于 $options
的作用可以查看官网说明 (opens new window)。
说明:
关于initInternalComponent
在我们当前案例中没有执行到,所以暂时不做分析,后面章节具体案例中我们在分析。
关于 mergeOptions
我们会在下一小节分析。
我们继续往下看,代码如下:
源码目录:src/core/instance/init.js
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这段代码是一些初始化方法,主要是初始化生命周期,初始化事件中心,初始化渲染,初始化 data
、props
、computed
、watcher
,初始化 provide/inject
等。
说明:
关于初始化我们放在下一章节单独分析。
我们再来看一下最后一度代码,如下:
源码目录:src/core/instance/init.js
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
2
3
这段代码是 挂载 的逻辑,若 Vue
实例上面有 el
属性则直接执行 vm.$mount(vm.$options.el)
挂载元素,若 Vue
实例上面没有 el
属性,则生命周期执行到这就挂起了,直到手动去执行 vm.mount(el)
,生命周期才会继续执行,接下来的重点就是 mount
这个方法究竟完成了哪些事情,这就是我们需要重点关注的。
# 3. 规范化
# 3.1 参数列表
接下来我们看看 mergeOptions
函数的调用,首先我们分析一下它的参数,如下:
resolveConstructorOptions(vm.constructor)
:当前实例构造函数的options
options
:Vue
实例化时传入的options
vm
:Vue
实例
我们在 vue源码分析(五) 静态属性和方法 (opens new window) 分析过了 Vue.options
最终为如下:
Vue.options = {
components: {
KeepAlive
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
2
3
4
5
6
7
8
9
10
11
12
13
而我们实例化 Vue
时传入的 options
,如下:
{
el: "#app",
template: "<div> {{ name }} </div>",
data: {
name: "robin"
}
}
2
3
4
5
6
7
所以 mergeOptions
的最终参数列表为,如下:
vm.$options = mergeOptions(
// resolveConstructorOptions(vm.constructor)
{
components: {
KeepAlive
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
},
// options || {}
{
el: "#app",
template: "<div> {{ name }} </div>",
data: {
name: "robin"
}
},
vm
)
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
说明:
关于 resolveConstructorOptions
我们放在组件化章节单独分析,在当前案例中我们只要知道返回的是 Vue.options
即可。
分析完 mergeOptions
的参数我们再来分析 mergeOptions
函数的定义,如下:
源码目录:src/core/util/options.js
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
* 将两个选项对象合并到一个新的对象中。用于实例化和继承的核心实用程序
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm) // 对props进行一次规范化
normalizeInject(child, vm) // 对inject进行一次规范化
normalizeDirectives(child) // 对directives进行一次规范化
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
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
# 3.2 检查组件
首先我们看一下这个函数的注释,如下:
源码目录:src/core/util/options.js
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
2
3
4
这句话的意思是将两个选项对象合并到一个新的对象中,用于实例化和继承的核心实用程序。这里要注意两点:
第一,这个函数将会产生一个新的对象;
第二,这个函数不仅仅在实例化对象(即
_init
方法中)的时候用到,在继承(Vue.extend
)中也有用到,所以这个函数应该是一个用来合并两个选项对象为一个新对象的通用程序。
接下来我们分析mergeOptions
的第一个 if
语句 ,如下:
源码目录:src/core/util/options.js
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
2
3
这段代码的作用是在开发环境,检查组件,参数是 child
,我们再来看 checkComponents
的定义,如下:
源码目录:src/core/util/options.js
/**
* Validate component names
*/
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
2
3
4
5
6
7
8
通过注释我们知道这个函数的作用是校验组件名称,首先 checkComponents
通过 for in
循环遍历 options.components
属性中所有的组件,拿到组件的名称,最后通过 validateComponentName
函数校验组件名称。
接下来我们找到 validateComponentName
的定义,如下:
源码目录:src/core/util/options.js
export function validateComponentName (name: string) {
if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
)
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
通过源码我们可以清楚的知道,组件的名称必须满足两个条件:
- 组件的名字由普通的字符和中横线(-)和一些特殊符号组成,且必须以字母开头。特殊符号包括
·À-ÖØ-öø-ͽͿ--‿-⁀⁰-Ⰰ-、-豈-﷏ﷰ-�
- 组件名字不能为内置的标签和保留标签
我们再来看看 isBuiltInTag
源码目录:src/shared/util.js
export const isBuiltInTag = makeMap('slot,component', true)
这段代码是判断标签是否为内置的标签有,目前 Vue
内置标签只有 slot
和component
。
关于 config.isReservedTag
我们再平台化的时候已经分析过了,可以查看 这里 (opens new window) 。
# 3.3 合并参数的类型
我们继续往下看,源码如下:
源码目录:src/core/util/options.js
if (typeof child === 'function') {
child = child.options
}
2
3
这段代码是检查传入的 child
是否是函数,如果是的话,取到它的 options
选项重新赋值给 child
。所以说 child
参数可以是普通选项对象,也可以是 Vue
构造函数和通过Vue.extend
继承的子类构造函数。
# 3.4 规范化props
我们继续往下看,源码如下:
源码目录:src/core/util/options.js
normalizeProps(child, vm)
我们在 Vue官网 (opens new window)可以知道,props
的类型为 Array<string> | Object
,即 props
类型有数组和对象两种,其中数组中的元素都是字符串,我们举个例子,如下:
// 数组
const child = {
props: ['age', 'msg']
}
// 对象
const child = {
props: {
age: Number,
msg: {
type: String,
default: ''
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
对应 props
值的类型有多种,但是在 Vue
内部处理的时候都统一规范化成一种类型,这样在选项合并的时候就能够统一处理,但是对于开发者来说,在使用的时候可以以多种写法使用。
所以接下来我们看看 Vue
内部是如何对 props
进行规范化的,代码如下:
源码目录:src/core/util/options.js
/**
* Ensure all props option syntax are normalized into the
* Object-based format.
*/
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
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
首先通过 options.props
获取所有的 props
,如果 props
不存在则直接返回,如果存在继续执行规范化代码。接着定义一些变量如 res
、i
、val
、 name
,其中:
res
用来存在规范化的最后结果i
用来存储props
的个数val
用来存储每个props
的值name
用来存储每个props
的名称
我们继续往下看,接下来是一个 if...else if...else if...
语句,首先是 if
语句判断 props
为数组的形式,然后通过一个 while
循环处理每一个 props
。while
循环中首先获取到数组的每个元素,然后判断元素的类型,如果类型为 string
则通过 camelize
将 props
中的每个元素中的横线转驼峰形式,然后在 res
对象上添加了与转驼峰后的 props
同名的属性,其值为 { type: null }
,这就是实现了对字符串数组的规范化,将其规范为对象的写法,只不过 type
的值为 null
。如果不是 string
类型在开发环境中报一个警告。
else if
语句是对 props
为对象的形式进行处理,首先通过 for...in...
循环获取到 props
中的所有元素的名称,然后通过对象获取值的方式即 props[key]
获取到每个名称对应的值,然后同样用camelize
将 props
中的每个元素名称中的横线转驼峰形式,最后是一个三元运算符,判断 val
如果是一个纯对象则直接将 val
赋值给res
对象上转驼峰后的 props
同名的属性,如果不是则把 val
包装成一个对象的形式赋值给res
对象上转驼峰后的 props
同名的属性。
最后如果 props
既不是数组也不是对象,则在开发环境报一个警告。
所以通过 normalizeProps
规范化,我们最终得到的 props
为,如下:
// 数组规范化后的值
const child = {
props: {
age: {
type: null
},
msg: {
type: null
}
}
}
// 对象规范化后的值
const child = {
age: {
type: Number
},
props: {
msg: {
type: String,
default: ''
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.5 规范化inject
我们继续往下看,源码如下:
源码目录:src/core/util/options.js
normalizeInject(child, vm)
我们在 Vue官网 (opens new window)可以知道,inject
的类型为 Array<string> | { [key: string]: string | Symbol | Object }
,即 inject
类型有数组和对象两种,其中数组中的元素都是字符串,我们举个例子,如下:
// 数组
const child = {
inject: ['age', 'msg']
}
// 对象
const child = {
inject: {
age: 'age1',
msg: {
from: 'bar',
default: 'foo'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
和props
类似, inject
值的类型有多种,但是在 Vue
内部处理的时候都统一规范化成一种类型,这样在选项合并的时候就能够统一处理,但是对于开发者来说,在使用的时候可以以多种写法使用。
所以接下来我们看看 Vue
内部是如何对 props
进行规范化的,代码如下:
源码目录:src/core/util/options.js
function normalizeInject (options: Object, vm: ?Component) {
const inject = options.inject
if (!inject) return
const normalized = options.inject = {}
if (Array.isArray(inject)) {
for (let i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] }
}
} else if (isPlainObject(inject)) {
for (const key in inject) {
const val = inject[key]
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "inject": expected an Array or an Object, ` +
`but got ${toRawType(inject)}.`,
vm
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
首先通过 options.inject
获取所有的 inject
,如果 inject
不存在则直接返回,如果存在继续执行规范化代码。接着定义一些变量如 normalized
其值和 options.inject
的值指向相同的引用,这样的好处是在接下来的的代码中修改了normalized
的值,options.inject
的值也会对应改变。
我们继续往下看,接下来是一个 if...else if...else if...
语句,首先是 if
语句判断 inject
为数组的形式,然后通过一个 for
循环处理每一个 inject
。for
循环中在 normalized
对象上添加了与 inject
同名的属性,其值为 { from: inject[i] }
。这就是实现了对字符串数组的规范化,将其规范为对象的写法。
else if
语句是对 inject
为对象的形式进行处理,首先通过 for...in...
循环获取到 inject
中的所有元素的名称,然后通过对象获取值的方式即 inject[key]
获取到每个名称对应的值,通过一个三元运算符,判断 val
如果是一个纯对象则直接将 val
赋值给normalized
对象上与 inject
同名的属性,如果不是则把 val
包装成一个对象的形式赋值给normalized
对象上与 inject
同名的属性。
最后如果 inject
既不是数组也不是对象,则在开发环境报一个警告。
所以通过 normalizeInject
规范化,我们最终得到的 inject
为,如下:
// 数组规范化后的值
const child = {
props: {
age: {
form: 'age'
},
msg: {
form: 'msg'
}
}
}
// 对象规范化后的值
const child = {
age: {
form: 'age1'
},
props: {
msg: {
from: 'bar',
default: 'foo'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.6 规范化directives
我们继续往下看,源码如下:
源码目录:src/core/util/options.js
normalizeDirectives(child)
我们在 Vue官网 (opens new window)可以知道,directives
的类型为 Object
,即 directives
类型只有对象一种,我们举个例子,如下:
<div v-attrs="{ color: 'white', text: 'hello!' }">
<input v-focus>
</div>
var vm = new Vue({
el: '#app',
data: {
msg: 'this is vue directives demo'
},
// 注册两个局部指令
directives: {
focus: {
inserted: function (el) {
el.focus()
}
},
attrs: function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
el.style.backgroundColor = binding.value.color
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
虽然 directives
只有对象一种类型,但是我们可以从上面的是案例中发现,directives
的值由多种写法,例如上面案例中 focus
是一个对象,而 attrs
是一个函数,既然出现了允许多种写法的情况,那么当然要进行规范化了。
所以接下来我们看看 Vue
内部是如何对 directives
进行规范化的,代码如下:
源码目录:src/core/util/options.js
function normalizeDirectives (options: Object) {
const dirs = options.directives
if (dirs) {
for (const key in dirs) {
const def = dirs[key]
if (typeof def === 'function') {
dirs[key] = { bind: def, update: def }
}
}
}
}
2
3
4
5
6
7
8
9
10
11
首先通过 options.directives
获取所有的 directives
,如果 directives
存在继续执行规范化代码。接下来是一个 for...in...
循环获取到 directives
中的所有元素的名称,然后通过对象获取值的方式即 dirs[key]
获取到每个名称对应的值,接下来判断注册的指令是一个函数的时候,则将该函数作为对象形式的 bind
属性和 update
属性的值。也就是说,可以把使用函数语法注册指令的方式理解为一种简写。
所以通过 normalizeDirectives 规范化,我们最终得到的 directives
为,如下:
var vm = new Vue({
el: '#app',
data: {
msg: 'this is vue directives demo'
},
// 注册两个局部指令
directives: {
focus: {
inserted: function (el) {
el.focus()
}
},
attrs: {
bind: function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
el.style.backgroundColor = binding.value.color
},
update: function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
el.style.backgroundColor = binding.value.color
}
}
}
})
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
至此我们就彻底分析完了这三个用于规范化选项的函数的作用了, props
、inject
以及 directives
这三个选项有了新的认识,同时也知道了 Vue
是如何做到允许开发者采用多种写法,也知道了 Vue
是如何统一处理的。接下来我们继续分析选项合并的其他逻辑。