Vue组件data定义必须是个函数?

458 阅读2分钟

1.问题:Vue组件data为什么必须是个函数而Vue的根实例则没有此限制?

    <div id="demo"> 
        <h1>test</h1> 
        <comp></comp> 
        <comp></comp> 
    </div> 
    Vue.component('comp', { 
            template:'<div @click="counter++">{{counter}}</div>',            
            data: {counter: 0} 
        })  
 // 创建实例 
        const app = new Vue({ 
            el: '#demo', 
        }); //这里会提示警告

2. 整体思路

  由于局部组件存的动态创建,过程比较复杂,这里使用全局组件来对比。
  1. vue初始化,先调用initAssetRegisters方法,把所有全局组件的构造函数先初始化好,并且全局组件合并到vue的配置信息上。
  2. 这过程中会触发mergeOptions的strats.data 方法判断如果是组件时候,判断data是否为function 如果不是则警告提示。
  3. 根Vue或者组件实例化时,都会统一调用initData(),初始化data。

3. 分析源码

  1. 从源码中分析   src/core/global-api/assets.js vue初始化的时候调用了initAssetRegisters方法
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
          // this.options._base 等价于访问 Vue根实例
          //这里最终调用Vue.extend方法,传入vue的component配置信息。
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}
  1. src/core/global-api/extend.js 定义了Vue.extend方法
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions( //这里把根Vue和当前组件的配置信息进行合并,返回当前组件的构造函数
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
 ...
  }
}
  1. src/core/util/options.js mergeOptions方法 进行合并
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)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  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)//这里通过vm是否有来判断是根实例,还是组件
  }
  return options
}

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)
}
  1. 最终,无论是根vue实例化,还是组件实例化,都会统一走initData初始化。先实例化根vue,再实例化组件。   src\core\instance\state.js initState -> initData()
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) //初始化
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) //当是函数时候,会执行getData函数返回对象data
    : data || {} //直接使用传入的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
    )
  ...
  }

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm) //这里只返回对象
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

4.总结:

  1. 在根Vue实例或者全局组件实例化时,会先把全局组件的构造函数初始化一次,然后通过initData统一实例化。
  2. 由于全局组件共用一个构造函数,所以在initData 初始化的时候,如果使用对象定义data,所有组件实例都会共用一个data对象,产生数据污染。而采用函数的方式通过工厂函数统一生成新data对象,有效规避的实例间共用污染问题。
  3. vue根实例全局只有一个,所以不会给其他实例修改,