Vue 组件 data 为什么必须是函数???

338 阅读2分钟

这个问题最基础的是先理解数据的引用类型(懂的人都懂)。vue 中组件的实例的创建都会先执行其构造函数,其中会执行 vue 的 init 方法进行组件的初始化,在initState 中会进行 data 的初始化,即initData()。

在源码中:src\core\instance\state.js -initData() 中 函数每次执行都会返回全新的 data 对象。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

关于 data 的核心代码如下:

  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  1. 如果 data 是函数,则执行工厂函数返回一个新的 data 对象
  2. 否则直接返回这个 data 对象。会导致所有的组件实例共用一个 状态对象,造成数据污染。

当然,vue 在框架中也加入了 data 的类型检测,发出报错提醒。

对于根实例,它是单例的,全局只会创建一个,所以不会出现上述问题。代码上:在 init 中进行 mergeOptions 的时候,会进行判断是否为自定义组件。

    // merge options
    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
      )
    }

在初始化组件的构造函数的过程中,会执行 Vue.extend 方法 进行 mergeOptions 选项合并,在合并的过程中,执行 mergeField 时会进行 data 的合并策略,即strats策略对象,在 src\core\util\options.js 中代码如下:

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    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)
}

在 data 的合并检测中会对 组件 data 进行判断并进行报错提醒,此时会根据 vm 区分跟实例和自定义组件,根实例才会有vm,所以在初始化跟实例的时候会跳过该检测逻辑!!!(单例不需要该逻辑限制)

结论

  1. Vue 组件可能存在多个实例,如果使用对象的形式定义 data,则会导致它们共用一个 data 对象,会造成该对象污染;
  2. 采用函数形式定义,在 initData 时会将其作为工厂函数返回全新的 data 对象,有效避免多实例之间的状态污染问题。
  3. Vue 跟实例创建过程中,不存在该限制,因为根实例只能有一个,不需要担心这种情况。