vue-component1 合并配置源码解析

59 阅读1分钟

案例

<body>
  <div id="app"></div>
</body>
<script src="vue.js"></script>
<script>
let childComp = {
  template: '<div>{{msg}}</div>',
  created() {
    console.log('child created')
  },
  mounted() {
    console.log('child mounted')
  },
  data() {
    return {
      msg: 'Hello Vue'
    }
  }
}

Vue.mixin({
  created() {
    console.log('parent created')
  }
})

let app = new Vue({
  el: '#app',
  render: h => h(childComp)
})
</script>

initGlobalAPI

当我们加载 vue 文件的时候,就执行了 initGlobalAPI(Vue) 方法:

  function initGlobalAPI(Vue) {
      // 像 Vue 定义了只读属性 config 和 util、set、nextTick 等属性
      ...
      Vue.options = Object.create(null);
      // 像 options 里添加了 components、directives、filters 三个空对象
      ASSET_TYPES.forEach(function (type) {
          Vue.options[type + 's'] = Object.create(null);
      });
      // 将 Vue 初始方法存储起来,在 _render 阶段会用于构造子类构造函数
      Vue.options._base = Vue;
      // 把一些内置组件 `<keep-alive>`、`<transition>` 和 `<transition-group>` 扩展到 `Vue.options.components` 上
      extend(Vue.options.components, builtInComponents);
      ...
      // 将 mixin 的内容合并到配置中
      initMixin() 
  }

合并配置

此时,传入的 options 将作为 mergeOptions 的第二个参数 child 传入,为:

{
  el: "#app"
  render: h => h(childComp)
}

安装代码的执行,在 _init() 方法中我们会合并配置,不是组件的情况下,会执行下面的方式进行合并:

if (options && options._isComponent) {
     initInternalComponent(vm, options);
} else {
    vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm);
}

resolveConstructorOptions 方法返回的就是我们在 initGlobalAPI 方法中定义的 options 属性:

{
 components: {}
 created: [ƒ]     // console.log('parent created')
 directives: {}
 filters: {}
 _base: ƒ Vue(options)
}

这个将作为 mergeOptions 的第一个参数 parent 传入。

非组件合并

  function mergeOptions(parent, child, vm) {
      // 格式化 child 的属性,递归把 `extends` 和 `mixins` 合并到 `parent` 上
      ...
      var options = {};
      var key;
      for (key in parent) {
          mergeField(key);
      }
      for (key in child) {
          if (!hasOwn(parent, key)) {
              mergeField(key);
          }
      }
      // 对不同的 `key` 有着不同的合并策略
      function mergeField(key) {
          var strat = strats[key] || defaultStrat;
          options[key] = strat(parent[key], child[key], vm, key);
      }
      return options;
  }

这段逻辑中比较重要且难以理解的是 mergeField 方法,我们先看一下 strats 对象里面的内容:

components: ƒ mergeAssets(parentVal, childVal, vm, key)
computed: ƒ (parentVal, childVal, vm, key)
created: ƒ mergeLifecycleHook(parentVal, childVal)
data: ƒ (parentVal, childVal, vm)
el: ƒ (parent, child, vm, key)
props: ƒ (parentVal, childVal, vm, key)
watch: ƒ (parentVal, childVal, vm, key)
...

这里面包含了 vue 中可接收的 option 的名称,并且对于不同的 key 有着不同的处理合并的方法,比如生命周期的方法使用的就是 mergeLifecycleHook 方法,componentdirectivesfilters 使用 mergeAssets 方法...

mergeLifecycleHook

我们以合并生命周期为例,看看是怎么进行合并的:

  function mergeLifecycleHook(parentVal, childVal) {
      var res = childVal
          ? parentVal
              ? parentVal.concat(childVal)
              : isArray(childVal)
                  ? childVal
                  : [childVal]
          : parentVal;
      return res ? dedupeHooks(res) : res;
  }

parentVal 传入的时候不是 undefined 就是数组,childVal 可能是 undefinedstring 或者数组,经过这个合并,如果 parentValchildVal 都有值,则会将两个值合并到一个数组中。

组件合并

此案例中传入 initInternalComponent 方法的 options 为:

  parent: Vue// div#app 父Vue实例
  _isComponent: true,
  _parentVnode: VNode  // vue-component-1 父VNode实例

initInternalComponent 方法首先执行 Object.create 使用现有的对象来作为新创建对象的原型,这里的 vm.constructor 就是子组件的构造函数 Sub

  function initInternalComponent(vm, options) {
      var opts = (vm.$options = Object.create(vm.constructor.options));
      // 简单一层对象赋值
      var parentVnode = options._parentVnode;
      opts.parent = options.parent;
      opts._parentVnode = parentVnode;
      ...
  }

这里的 vm 是一个 vue组件类型的实例,它的 Prototype 是 Vue 类型的实例,其中很多 options 定义在 Prototype 上,我们访问这些属性的时候由于作用域链,所以和访问 vm 上的属性一致。 最后得到的 vm.$options 的值差不多是如下这样:

vm.$options = {
  parent: Vue,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode,
  _renderChildren:undefined,
  __proto__: {
    components: { },
    directives: { },
    filters: { },
    _base: function Vue(options) {
        //...
    },
    _Ctor: {},
    created: [
      function created() {
        console.log('parent created')
      }, function created() {
        console.log('child created')
      }
    ],
    mounted: [
      function mounted() {
        console.log('child mounted')
      }
    ],
    data() {
       return {
         msg: 'Hello Vue'
       }
    },
    template: '<div>{{msg}}</div>'
  }
}