# Vue中data为何必须是一个函数
# 源码层面
data的判断是在mergeOptions中的strats.data策略函数中进行的:
- 根组件options合并,第三个参数vm为组件实例。而子组件进行options合并时,vm为
undefined。所以在调用strats.data策略函数,进行data选项的合并时,如果data不是function,则报错。
# 根组件options合并
根组件的合并是在根组件_init的时候,此时mergeOptions的第三个参数vm为当前实例,所以data不需要是函数。
Vue.prototype._init = function (options?: Object) {
// ...
// merge options
if (options && options._isComponent) { // 子组件初始化
//
initInternalComponent(vm, options)
} else {
// 根组件,options合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 子组件options合并
在render阶段生成vnode时,内部调用过程如下:
内部调用
_createElement->createElement生成vnode;调用
createElement时,如果tag是非内置html字符串,或者是一个对象,表示这是一个自定义组件,此时调用createComponent('my-component')方法,来创建自定义组件vnode;在
createComponent内部,如果发现Ctor是一个对象,则调用Ctor = baseCtor.extend(Ctor),这里的baseCtor就是Vue,即调用Vue.extend(Ctor)方法将Ctor对象通过Vue.extend方法,返回一个子类构造函数对象。Vue.extend方法内部,会对options进行合并。
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// data就是选项对象
if (isObject(Ctor)) { // 如果Ctor是对象,则通过Vue.extend改造为Sub子类
// 调用Vue.extend(Ctor), Ctor等于Sub子类,继承自Vue,代码位置:src/core/global-api/extend.js
Ctor = baseCtor.extend(Ctor)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
而在mergeOptions方法中,在进行data选项合并时,调用strats.data测量方法时,如果vm为undefined,那么data就必须是一个函数,否则报错。
// extend.js
Vue.extend = function (extendOptions: Object): Function {
// ...
// 调用mergeOptions方法,第三个参数vm未传
Sub.options = mergeOptions(
Super.options,
extendOptions
)
}
2
3
4
5
6
7
8
9
10
strats.data策略函数:
// util/options.js
// data合并
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// vm为undefined时,如果data不是一个函数,则报错.主要是针对子组件
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 逻辑层面
- 根组件只有一个。不会出现多个实例共享的情况
- 子组件可能复用多次。在调用
Vue.extend返回子组件构造函数时,由于它们使用的同一个options选项。如果options中的data是一个对象,那么不同的组件实例,将共用一份data数据,将会造成不同组件间数据相互影响。因此,对应自组件而言,要求data是一个工厂函数,每个组件生产自己独有的数据。
建议:
通过调试源码的方式,查看根组件、子组件调用
mergeOptions的时机。
假设子组件的data是一个对象,如下:
<template>
<div class="demo">
my-component
</div>
</template>
<script>
export default {
data() {
name: 'my-component'
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
那么,在子组件创建vnode阶段,会调用Vue.extend生成子类实例Sub(options是同一个)
注意:同一个组件,创建的子类实例只有一个,Vue.extend内部会缓存。
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
// ...
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 将options存放到Sub.options上,此时Sub.options上有data属性
Sub.options = mergeOptions(
Super.options,
extendOptions
)
return Sub
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在子组件挂载阶段,先实例化子组件实例。即new Sub的时候,执行_init初始化函数:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// merge options
if (options && options._isComponent) { // 子组件options合并,
initInternalComponent(vm, options)
} else { // 根组件初始化,合并options
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
}
// ...
initState(vm);
// ...
}
2
3
4
5
6
7
8
9
10
11
12
而在initInternalComponent内部,初始化子组件的vm.$options,并通过原型链,建立与父类Sub.options之间的关联。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// 将子组件options,通过原型链与构造函数关联
const opts = vm.$options = Object.create(vm.constructor.options)
// ...
}
2
3
4
5
因此,在后续执行initState-->initData初始化data时,对data的取值都是来自于父类的Sub.options。因此就会出现,多个实例,使用同一份data数据的情况,子组件之间就会相互之间产生影响。
function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.data) { // 设置data选项的响应式
initData(vm)
}
}
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 案例data为对象
前置条件:
需要注释掉源码src/core/utils/options中strats.on函数中对于子组件的判断:该判断中,如果是子组件,并且内部data不是function,则直接忽略了子组件的data值,取parentVal值。
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// 对于子组件,如果data不是一个function,则直接忽略子组件data值,取parentVal值返回。
// if (childVal && typeof childVal !== 'function') {
// process.env.NODE_ENV !== 'production' && warn(
// 'The "data" option should be a function ' +
// 'that returns a per-instance value in component ' +
// 'definitions.',
// vm
// )
// return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
案例代码:
// my-component.vue
<template>
<div class="demo">
<p @click="click">name: {{ name }}</p>
</div>
</template>
<script>
export default {
data: {
name: 'my-component'
},
methods: {
click() {
this.name+= '1';
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div id="app">
<my-component></my-component>
<my-component></my-component>
</div>
</template>
<script>
import MyComponent from './components/my-component.vue';
export default {
name: 'appVue',
components: {
MyComponent
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
效果:当我们点击某一个子组件的时候会发现,两个子组件的数据同时变化了。

# 伪代码演示
function Vue() {}
// 返回组件子类
Vue.extend = function(extendOptions) {
let Super = this;
function Sub(options) {
this._init(options);
}
Sub.options = mergeOptions(Super.options, extendOptions);
return Sub;
}
Vue.prototype._init = function() {
let vm = this;
// 自组件,通过原型链关联Sub.options
vm.$options = Object.create(vm.constructor.options);
this.data = vm.$option.data;
}
// data是一个对象时,则两个子类实例,共享一份data数据。
let Sub = Vue.extend({
data: {
name: 'xxx'
}
});
let s1 = new Sub();
let s2 = new Sub();
s1.data.name = 'yyy';
// 修改s1实例数据
console.log(s1.data); // { name: 'yyy' }
// s2实例被影响了
console.log(s2.data); // { name: 'yyy' }
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