Vue源码解析问题(2.根实例、组件的data格式不同)

227 阅读1分钟

2. 根实例的data是对象,为什么组件的data是函数,组件不配拥有 对象么?

创建demo

<!DOCTYPE html>
<html>

<head>
  <title>Vue_data</title>
<body>
  <div id="demo">
    <h3>组件data为啥是函数?</h3>
    <subcomponents></subcomponents>
    <subcomponents></subcomponents>
    {{counter}}
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    // 创建实例
    Vue.component('subcomponents', {
      template: '<div @click="childCounter++">{{childCounter}}</div>',
      /* data: { // 抛异常 The "data" option should be a function that returns a per-instance value in component definitions.
        childCounter: 0
      } */
      data () {
        return {
          childCounter: 0
        }
      }
    })
    const app = new Vue({
      el: '#demo',
      data: {
        counter: 1
      }
    });
    console.log(app.$options.render);
  </script>
</body>
</html>

源码位置

url src\core\instance\init.js 找到 initState => initData

init.js
// expose real self
vm._self = vm
initLifecycle(vm) // 生命周期初始化 实例属性初始化 $parent $root $refs $children
initEvents(vm) // 自定义事件处理
initRender(vm) // 插槽解析 $slots $createElement()
 // 此时并未有数据
callHook(vm, 'beforeCreate')
// 接下来都是和组件状态相关的数据操作
initInjections(vm) // 注入祖辈传递的数据
initState(vm) // 找到这个宝宝 ------------------------------------- I AM HERE!!
initProvide(vm) // 传递给后代,用来隔代传递参数
callHook(vm, 'created')
-----------------------------------------------------------------------------------------
state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 优先级 props > methods > data > computed > watch
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 设置data 走data
    initData(vm) // ---------------------------------------------- I AM HERE!!
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
-------------------------------------------------------------------------------------------
state.js
function initData (vm: Component) {
  let data = vm.$options.data
  // 如果data是函数,则执行函数 并将其返回的结果作为data选项的值
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) //  我就是棒打鸳鸯的代码
    : data || {}
  // proxy data on instance
  // 校验,避免命名冲突
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  while (i--) {
  ---
  }
  // observe data
  // 递归响应式处理
  observe(data, true /* asRootData */)
}

结论: 如果组件定义对象data, data将被指向相同的对象,会造成数据污染。因此多实例时,为避免数据污染,通过工厂函数返回一个新对象。
但是就会衍生问题,为啥根实例的data可以是对象
先去思考下,组件和跟实例声明的方式是不是不相同,根实例是通过new Vue创建的,是否是因为Vue只被创建一次,根据这个判断,我去源码中找答案

源码位置

url src\core\instance\init.js

// 合并选项: new Vue时,用户配置和系统配置合并
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
  )
}
---------------------------------------------------
处理data
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) // 组件实例没有vm
  }
  // 根实例存在vm
  return mergeDataOrFn(parentVal, childVal, vm)
}

结论: Component => 构造函数(用来把Vue的一些方法合并到当前实例上)此时当前的组件实例并没有被创建,在合并组件时,检验data 根实例 => 构造函数 此时有vm

总结

  1. Vue组件可能存在多实例,如果使用对象定义data,会导致共用一个data对象,那么当状态发生改变会影响所有组件实例,由此initData时,用使用工厂函数返回全新的对象,避免数据污染
  2. 当Vue根实例创建时,会存在实例,而组件实例被创建时,并没有实例,并且根实例只有一个,所以不存在数据污染