# 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
      )
    }
}
1
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)
  }
}
1
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
    )
}

1
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
      )
    }
  }
}
1
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>
1
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
  }
}
1
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);
    // ...
  }
1
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)
  // ...
}
1
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 || {}
}
1
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)
}
1
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>
1
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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

效果:当我们点击某一个子组件的时候会发现,两个子组件的数据同时变化了。

vue data is object

# 伪代码演示

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' }

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