vue源码分析(二十) 数据响应系统 —— props、methods
# 1. 概述
在前面的章节中,我们已经讲解了 initState
函数中的 initData
、initComputed
和 initWatch
,到目前为止整个 initState
函数中我们还剩下 props
以及 method
等选项的初始化和实现没有讲解,本章节我们将继续分析剩余选项的初始化及实现。
# 2. Props初始化
首先我们看一下 props
选项的初始化及实现,代码如下:
源码目录:scr/core/instance/state.js
if (opts.props) initProps(vm, opts.props)
通过上面代码我们可以看出,只有当 opts.props
选项存在时才会调用 initProps
函数进行初始化工作。initProps
函数与其他选项的初始化函数类似,接收两个参数分别是组件实例对象 vm
和选项 opts.props
。
说明:关于 props
选项可以移步 这里 (opens new window) 学习。
我们在 规范化props (opens new window) 已经分析过,props
最终被规范化为一个对象,并且该对象每个属性的键名就是对应 prop
的名字,而且每个属性的值都是一个至少会包含一个 type
属性的对象。例如:
// 数组规范化后的值
const child = {
props: {
age: {
type: null
},
msg: {
type: null
}
}
}
// 对象规范化后的值
const child = {
props: {
age: {
type: Number
},
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
# 2.1 initProps
接下来我们来分析 initProps
函数,如下:
源码目录:scr/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// 省略...
}
2
3
4
5
6
7
8
9
10
initProps
函数,首先定义了 propsData
常量,如果 vm.$options.propsData
存在,则使用 vm.$options.propsData
的值作为 propsData
常量的值,否则 propsData
常量的值为空对象。
propsData
就是 props
数据,我们知道组件的 props
代表接收来自外界传递进来的数据,这些数据总要存在某个地方,使得我们可以在组件内使用,而 vm.$options.propsData
就是用来存储来自外界的组件数据的。
接下来定义了 props
常量和 vm._props
属性,它和 vm._props
属性具有相同的引用并且初始值为空对象。
结下来定义了常量 keys
,同时在 vm.$options
上添加 _propKeys
属性,并且常量 keys
与 vm.$options._propKeys
属性具有相同的引用,且初始值是一个空数组。
最后定义isRoot
常量用来标识是否是根组件,因为根组件实例的 $parent
属性的值是不存在的,所以当 vm.$parent
为假时说明当前组件实例是根组件。
我们继续往下看,代码如下:
源码目录:scr/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
// 省略...
if (!isRoot) {
toggleObserving(false)
}
// 省略...
toggleObserving(true)
}
2
3
4
5
6
7
8
接下来是一个 if
语句块, 只要当前组件实例不是根节点,那么该 if
语句块内的代码将会被执行,即调用 toggleObserving
函数并传递 false
作为参数。
我们再来看看 toggleObserving
的定义,如下:
源码目录:src/core/observer/index.js
/**
* In some cases we may want to disable observation inside a component's
* update computation.
*/
export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
shouldObserve = value
}
2
3
4
5
6
7
8
9
通过上面代码我们知道, toggleObserving
函数是一个开关,因为它能修改 shouldObserve
变量的值。
我们再回到 initProps
函数,所以这个 if
语句的作用就是关闭数据检测。在 initProps
最后又打开数据检测。下面我们分析一下这样做的具体原因,我们继续往下看,代码如下:
源码目录:scr/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
// 省略...
for (const key in propsOptions) {
keys.push(key)
// 获取props的值
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 省略...
} else {
defineReactive(props, key, value)
}
}
// 省略...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
说明:关于 validateProp
我们后面单独分析。
首先该 for...in
循环所遍历的对象是 propsOptions
,它就是 props
选项参数,我们前面分析了它的格式,所以 for...in
循环中的 key
就是每个 prop
的名字。
for...in
循环体内首先将 prop
的名字(key
)添加到 keys
数组中,我们知道常量 keys
与 vm.$options._propKeys
属性具有相同的引用,所以这等价于将 key
添加到 vm.$options._propKeys
属性中。
接着定义了 value
常量,该常量的值为 validateProp
函数的返回值。 validateProp
函数的作用:用来校验名字(key
)给定的 prop
数据是否符合预期的类型,并返回相应 prop
的值(或默认值)。也就是说常量 value
中保存着 prop
的值。
接着是一个 if...else
语句块,我们先来分析 else
分支,else
中使用 defineReactive
函数将 prop
定义到常量 props
上,我们知道 props
常量与 vm._props
属性具有相同的引用,所以这等价于在 vm._props
上定义了 prop
数据。
通过前面章节的分析我们知道 defineReactive
的作用是将数据对象的数据属性转换为访问器属性。
源码目录:src/core/observer/index.js
// defineReactive
let childOb = !shallow && observe(val)
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 省略...
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
// 省略...
return ob
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
通过上面代码我们知道在使用 defineReactive
函数定义属性时,会调用 observe
函数对值继续进行观测。但由于之前使用了 toggleObserving(false)
函数关闭了开关,所以上面高亮代码中调用 observe
函数是一个无效调用。所以我们可以得出一个结论:在定义 props
数据时,不将 prop
值转换为响应式数据,这里要注意的是:由于 props
本身是通过 defineReactive
定义的,所以 props
本身是响应式的,但没有对值进行深度定义。为什么这样做呢?很简单,我们知道 props
是来自外界的数据,或者更具体一点的说,props
是来自父组件的数据,这个数据如果是一个对象(包括纯对象和数组),那么它本身可能已经是响应式的了,所以不再需要重复定义。另外在定义 props
数据之后,又调用 toggleObserving(true)
函数将开关开启,这么做的目的是不影响后续代码的功能,因为这个开关是全局的。
只有当不是根组件的时候才会关闭开关,这说明如果当前组件实例是根组件的话,那么定义的 props
的值也会被定义为响应式数据。
通过以上内容的讲解,我们应该知道的是 props
本质上与 data
是相同的,区别就在于二者数据来源不同,其中 data
数据定义的组件自身,我们称其为本地数据,而 props
数据来自于外界。
我们再来看一下 if...else
语句块中 if
分支,代码如下:
源码目录:scr/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
// 省略...
for (const key in propsOptions) {
// 省略...
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
// 省略...
}
}
// 省略...
}
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
在非生产环境下会执行 if
语句块的代码,首先使用 hyphenate
将 prop
的名字转为连字符加小写的形式,并将转换后的值赋值给 hyphenatedKey
常量,紧接着又是一个 if
条件语句块,其条件是在判断 prop
的名字是否是保留的属性(attribute
),如果是则会打印警告信息,警告你不能使用保留的属性(attribute
)名作为 prop
的名字。
接着使用了 defineReactive
函数定义 props
数据,可以看到与生产环境不同的是,在调用 defineReactive
函数时多传递了第四个参数,我们知道 defineReactive
函数的第四个参数是 customSetter
,即自定义的 setter
,这个 setter
会在你尝试修改 props
数据时触发,并打印警告信息提示你不要直接修改 props
数据。
我们继续往下看,代码如下:
源码目录:scr/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
// 省略...
for (const key in propsOptions) {
// 省略...
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
// 省略...
}
2
3
4
5
6
7
8
9
10
11
12
13
上面代码目的就是在组件实例对象上定义与 props
同名的属性,使得我们能够通过组件实例对象直接访问 props
数据,但其最终代理的值仍然是 vm._props
对象下定义的 props
数据。
只有当 key
不在组件实例对象上以及其原型链上没有定义时才会进行代理,这是一个针对子组件的优化操作,对于子组件来讲这个代理工作在创建子组件构造函数时就完成了,即在 Vue.extend
函数中完成的,这么做的目的是避免每次创建子组件实例时都会调用 proxy
函数去做代理,由于 proxy
函数中使用了 Object.defineProperty
函数,该函数的性能表现不佳,所以这么做能够提升一定的性能指标。
# 2.2 validateProp
# 3. initMethods
首先我们看一下 methods
选项的初始化及实现,代码如下:
源码目录:scr/core/instance/state.js
if (opts.methods) initMethods(vm, opts.methods)
通过上面代码我们可以看出,只有当 opts.methods
选项存在时才会调用 initMethods
函数进行初始化工作。initMethods
函数与其他选项的初始化函数类似,接收两个参数分别是组件实例对象 vm
和选项 opts.methods
。
说明:关于 methods
选项可以移步 这里 (opens new window) 学习。
接下来我们来分析 initMethods
函数,如下:
源码目录:scr/core/instance/state.js
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
// 省略....
}
}
2
3
4
5
6
initMethods
函数,首先定义常量 props
值为 vm.$options.props
的引用。然后是通过 for...in
循环遍历 methods
选项对象,其中 key
就是每个方法的名字。
我们继续往下看,代码如下:
源码目录:scr/core/instance/state.js
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') { // 事件不是方法的话报错
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
//如果props属性中定义了key,则在methods中不能定义同样的key
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
//isReserved 检查一个字符串是否以$或者_开头的字母
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
// 省略....
}
}
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
for...in
循环体里面首先判断在非生产环境下,执行一些警告,主要有以下几点:
- 检测该方法是否真正的有定义,如果没有定义则打印警告信息,提示开发者是否正确地引用了函数;
- 检测
methods
选项中定义的方法名字是否在props
选项中有定义,如果有的话则需要打印警告信息提示开发者:方法名已经被用于prop
;我们在前几章节的分析知道props
选项的初始化要先于methods
选项,并且每个prop
都需要挂载到组件实例对象下,如此一来methods
选项中的方法名字很有可能与props
选项中的属性名字相同,这样会导致覆盖的问题; - 检测方法名字
key
是否已经在组件实例对象vm
中有了定义,并且该名字key
为保留的属性名,如果为真则需要打印警告信息提示开发者:方法名与Vue内置方法冲突。根据isReserved
函数可知以字符$
或_
开头的名字为保留名;
我们继续往下看,代码如下:
源码目录:scr/core/instance/state.js
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
// 省略....
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
2
3
4
5
6
7
通过这句代码可知,之所以能够通过组件实例对象访问 methods
选项中定义的方法,就是因为在组件实例对象上定义了与 methods
选项中所定义的同名方法,当然了在定义到组件实例对象之前要检测该方法是否是函数类型:typeof methods[key] !== 'function'
,如果没有则添加一个空函数到组件实例对象上。