组件化的合并配置 - 学习vue源码系列 3.2

简介: 组件化的合并配置 - 学习vue源码系列 3.2

组件化的合并配置 - 学习vue源码系列 3.2


决定跟着黄轶老师的 vue2 源码课程好好学习下vue2的源码,学习过程中,尽量输出自己的所得,提高学习效率,水平有限,不对的话请指正~

vue 的源码clone 到本地,切换到分支2.6

Introduction

组件化渲染的那块,我个人觉得挺复杂的,我还没有捋顺,我决定先往后看,看完整体,再来磕硬骨头。

其实 vue 也有点配置为王的感觉,它的配置就是options

后期组件的各种相关操作,都围绕options展开。

本篇着重看看,vue是怎么处理options的,最最重点的是,怎么合并各个options的。

先看 demo

尝试先自己想想,这些钩子函数里的这些打印顺序如何。

<div id="app"></div>
<script src="/Users/zhm/mygit/vue/dist/vue.js"></script>
<script>
  const log = console.log;
  // 全局mixin
  Vue.mixin({
    created() {
      log("mixin created");
    },
  });
  // 子组件实例
  let childCompInstance = null;
  // 子组件
  let childComp = {
    name: "MSG",
    template: "<div>{{msg}}</div>",
    created() {
      childCompInstance = this;
      log("child created");
    },
    data: () => ({ msg: "Hello Vue" }),
  };
  // 根组件实例
  let vueInstance = new Vue({
    el: "#app",
    created() {
      log("parent created");
    },
    render: (h) => h(childComp),
  });
  log("Vue构造器上的options", Vue.options);
  log("Vue实例的options", vueInstance, vueInstance.$options);
  log(
    "VueComponent实例的options",
    childCompInstance,
    childCompInstance.$options,
    childCompInstance.$options.__proto__
  );
</script>

公布答案:

mixin created
parent created
mixin created
child created

其实定义在 Vue 的 mixin 里的options,会合并Vue构造器的的options里面。

用构造器创建实例的时候,构造器上面的options会和实例的options再次合并。

组件构造器是 Vue 的子类,合并的时候,主要的optionschildCompInstance.$options.__proto__里。

看下,例子里后面的打印:

网络异常,图片无法展示
|

Vue.mixin 和 Vue 构造器的 options 合并

先说下Vue.mixin是个函数,其传入的options,首先和 Vue 构造器上面的options合并。

Vue.mixin执行之后,Vue 构造器上面的options就有了其对应的值。

先来个超简单的示意:

/** vue源码 开始 */
function Vue(options) {
}
Vue.options = {};
// 将两个options合并成一个options
const mergeOptions = (parentOptions, childOptions) => {
  let options = {};
  // 生命周期的钩子合并的时候都是数组
  for (let key in childOptions) {
    if (key === "created") {
      const parentCreated = Array.isArray(parentOptions.created)
        ? parentOptions.created
        : parentOptions.created
        ? [parentOptions.created]
        : [];
      const childCreated = Array.isArray(childOptions.created)
        ? childOptions.created
        : [childOptions.created];
      options.created = [...parentCreated, ...childCreated];
    }
  }
  return options;
};
Vue.mixin = function mixin(options) {
  this.options = mergeOptions(Vue.options, options);
  return this
};
/** vue源码 结束 */
// 使用的例子
Vue.mixin({
  created() {
    console.log("mixin created");
  },
});
// {created:[x]}
console.log(Vue.options);

总的来说就是做着上面的事情,然后看 vue 的真正源码

// src/core/global-api/mixin.js
export function initMixin(Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // mixin就是{ created(){} }, this指的是Vue构造器,因为使用的时候是Vue.mixin
    this.options = mergeOptions(this.options, mixin);
    return this;
  };
}
// src/core/util/options.js
export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // parent就是Vue构造器的options,child就是{ created(){} }
  if (process.env.NODE_ENV !== "production") {
    checkComponents(child);
  }
  if (typeof child === "function") {
    child = child.options;
  }
  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirectives(child);
  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  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;
  // parent就是Vue构造器上面的options,key就是每项键,key不一样合并的策略不一样,先处理parent
  for (key in parent) {
    mergeField(key);
  }
  // child就是 { created(){} },在处理child里的(parent没有的)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);
  }
  return options;
}

Vue 实例的时候

Vue 构造器上面的options会和实例里参数options合并。 当然合并的逻辑是相似的。 拿这个例子来说,会变成created:[fn1,fn2],注意构造器的在前面,参数的在后面。

// options就是类似例子的{el:'#app',....}
Vue.prototype._init = function (options) {
  var vm = this;
  // 将各种选项合并,合并之后就是{created:[fn1,fn2]}
  vm.$options = mergeOptions(
    // 这里vm.constructor就是Vue,Vue.options{created:[fn1]}
    resolveConstructorOptions(vm.constructor),
    // {created:fn2}
    options || {},
    vm
  );
};

这里可以简单看下Vue上面的静态属性options

const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
  // 将内置组件扩展到Vue.options.components上,keepAlive transition
  extend(Vue.options.components, builtInComponents)
  // ...
}

VueComponent实例的时候

由于组件的构造函数是通过Vue.extend继承自Vue的。

Vue.extend = function (extendOptions: Object): Function {
  // ...
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  // ...
  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)
  // ...
  return Sub
}

很显然,组件类的options是组件对象和Vueoptions合并的。

组件初始化的时候,

// options有以下属性
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}

vnode.componentOptions.Ctor 就是指向 Vue.extend 的返回值 Sub。执行new的话,就会再次执行this._init(options).

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // 组件类的options和Vue类的options并不相同。
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode
  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

initInternalComponent 方法执行,相当于 vm.$options = Object.create(Sub.options)。 此时,vm.$options如下:

vm.$options = {
  parent: Vue /*父Vue实例*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父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>'
  }
}

走一遍看看调试

vue.js3处打个断点:

  • Vue.mixin打个断点
  • Vue.prototype._initoptions处打个断点
  • function mergeOptions打个断点

先看看mixin:

网络异常,图片无法展示
|

Vue.mixin之后,Vue的options合并了Vue.mixin的参数,变成这样:

{
  components: {}
  created: [ƒ]
  directives: {}
  filters: {}
  _base: ƒ Vue(options)
}

new Vue(options)会再和Vue的options合并,VueComponentoptions也会和Vue的options合并:

网络异常,图片无法展示
|

vue实例的$options是用户传的optionsVueoptions的合并,此时vue实例的$options

{
  components: {}
  created: (2) [ƒ, ƒ]
  directives: {}
  el: "#app"
  filters: {}
  render: (h) => h(childComp)
  _base: ƒ Vue(options)
  _isVue: true
  _uid: 0
}

VueComponent组件实例的options是和Vue的options合并:

网络异常,图片无法展示
|

显然组件的合并是先走了Vue.extend,然后走了initInternalComponent,最后,组件实例的options就是

_proto__:{
    components: {MSG: ƒ}
    created: (2) [ƒ, ƒ]
    data: () => ({ msg: "Hello Vue" })
    directives: {}
    filters: {}
    name: "MSG"
    template: "<div>{{msg}}</div>"
    _Ctor: {0: ƒ}
  }
  parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
  propsData: undefined
  render: ƒ anonymous( )
  staticRenderFns: []
  _componentTag: undefined
  _parentListeners: undefined
  _parentVnode: VNode {tag: "vue-component-1-MSG", data: {…}, children: undefined, text: undefined, elm: div, …}

总结

Vue 初始化阶段对于 options 的合并有 2 种方式:

  • 外部初始化 Vue 通过 mergeOptions 的过程,合并完的结果保留在 vm.$options 中。
  • 子组件初始化过程通过 initInternalComponent 方式,比mergeOptions的方式要快

纵观一些库、框架的设计几乎都是类似的,

  • 自身定义了一些默认配置,
  • 同时又可以在初始化阶段传入一些自定义配置,
  • 然后 merge配置,来达到定制化不同需求的目的。

引用

目录
相关文章
|
2天前
|
JavaScript
vue页面加载时同时请求两个接口
vue页面加载时同时请求两个接口
|
2天前
|
Web App开发 编解码 JavaScript
【Vue篇】Vue 项目下载、介绍(详细版)
【Vue篇】Vue 项目下载、介绍(详细版)
9 3
|
2天前
|
JavaScript
vue打印v-model 的值
vue打印v-model 的值
|
2天前
|
移动开发 前端开发 JavaScript
VUE3内置组件Transition的学习使用
VUE3内置组件Transition的学习使用
|
2天前
|
移动开发 JavaScript 前端开发
学习vue3使用在线官方开发环境play.vuejs.org进行测试
学习vue3使用在线官方开发环境play.vuejs.org进行测试
10 1
|
2天前
|
JavaScript
Vue实战-组件通信
Vue实战-组件通信
7 0
|
2天前
|
JavaScript
Vue实战-将通用组件注册为全局组件
Vue实战-将通用组件注册为全局组件
7 0
|
2天前
|
JavaScript 前端开发
vue的论坛管理模块-文章评论02
vue的论坛管理模块-文章评论02
|
2天前
|
JavaScript Java
vue的论坛管理模块-文章查看-01
vue的论坛管理模块-文章查看-01