Vue源码解析: 丰富的选项合并策略__Vue.js
发布于 3 年前 作者 banyungong 754 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

简介

Vue的初始化过程中,最开始的阶段就是选项合并阶段。它通过调用mergeOptions函数将两个选项配置合并成一个选项配置。这里的选项options的形式实际上就是我们平时开发时在Vue中写的对象配置,形式如下:

 { 
   components: {}, 
   filters: {},
   data() { return {} }, 
   computed: {}, 
   created: {}, 
   methods: {},
   ... 
 }

因此,选项合并实际可以简单的看作是两个上面的对象合并成一个对象。

由于mergeOptions是实现实例化(new Vue(options))、继承(Vue.extend)和混入(Vue.mixin)三大功能的核心函数,所以分析它的实现是理解Vue实例化过程和继承的必经之路。 下面我们将从以下几个方面来全面了解Vue中的选项合并:

  1. 实例化过程中的选项,了解我们需要合并的选项是怎样的。
  2. mergeOptions的实现,了解各个合并策略。
  1. 继承(Vue.extendextends:{})的选项合并。
  2. 混入(Vue.mixinmixins:[])的选项合并。
  1. 为什么实例化过程中有时用initInternalComponent而不是mergeOptions

实例化过程中的选项

从上一节的学习我们知道,Vue的实例化过程调用的是core/instance/init.js文件中的_init方法。

 Vue.prototype._init = function (options?: Object) {
     const vm: Component = this
   
     ...
   
     vm.$options = mergeOptions(
       resolveConstructorOptions(vm.constructor),
       options || {},
       vm
     )
   
     ...
   }

实例化的过程中第一个重要的处理就是选项的合并,这里第二个参数比较容易理解,就是我们平时写的Vue的配置项。第一个参数则是通过resolveConstructorOptions(vm.constructor)生成,找到resolveConstructorOptions方法,代码如下

 // * 返回构造函数的 options
 export function resolveConstructorOptions (Ctor: Class<Component>) {
   // * 如果不是继承,options 就是原构造函数的 options
   // * 如果是继承时,options 为合并 superOptions 和 extendOptions 的 options
   // * 此外,这里的 options 还包含了全局注册的 组件/指令/过滤器
   let options = Ctor.options
   // * Ctor.super 存在说明是调用了 extend 方法进行继承生成的构造函数
   // * 详见 /src/core/global-api/extend.js 文件
   // * - superOptions 是父类 options
   // * - extendOptions 是当前类传入的 options (如果与 sealedOptions不同,需要合并)
   // * - options = mergeOptions(superOptions, extendOptions)
   // * - sealedOptions 保存的是当前类继承时 合并后的 options(是extend的时候赋值的)
   if (Ctor.super) {
     const superOptions = resolveConstructorOptions(Ctor.super)
     const cachedSuperOptions = Ctor.superOptions
     // * 如果 superOptions 变动了,需要处理新的 options
     if (superOptions !== cachedSuperOptions) {
       Ctor.superOptions = superOptions
       // * sealedOptions 是 seal 的时候赋值的,
       // * 这里的变动可能是 options 在 extend 后继续被赋值
       // * 复现:https://jsfiddle.net/vvxLyLvq/2/
       // * 所以需要找出变动了的属性,然后更新到 extendOptions 上
       // * 这里的 extend 只是对象的合并
       const modifiedOptions = resolveModifiedOptions(Ctor)
       if (modifiedOptions) {
         extend(Ctor.extendOptions, modifiedOptions)
       }
       // * 由于 options 变化了,重新合并一次
       options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
       if (options.name) {
         // * 将自身的构造函数也存到了 components 对象中
         options.components[options.name] = Ctor
       }
     }
   }
   return options
 }

这里传入的是当前构造函数,那么Ctor.options指的是Vue构造函数的options,还记得上一节中提到的Vue.options吗?这里将options的内容从上一节中复制过来,代码如下:

 // Vue.options 内容
 {
   components: {
     KeepAlive,
     // 新增 platformComponents
     Transition,
     // 新增 platformComponents
     TransitionGroup
   },
   filters: {},
   directives: {
     // 新增 platformDirectives
     model,
     // 新增 platformDirectives
     show
   },
   _base: Vue
 }

再看下一句Ctor.super的判断,super这个字段是在core/global-api/extend.js文件中的extend方法调用时添加的。如果Ctor.super存在,说明Ctor是通过继承而来的子构造函数。但是,如果在extend后,我们又在父构造函数的options上添加新的属性,这个时候子构造函数是无法继承新的属性的。因此,这里需要通过Ctor.super向上寻找,找出所有父构造函数更新的options属性,并更新到子构造函数上,这样就能解决Vue.options被更改的问题了。 有兴趣的话,可以看一下Vueissues#4976

最后,经过resolveConstructorOptions处理后,最终得到的同样是一个Vue的配置选项,下一步则是需要将这两个配置选项进行合并了。

mergeOptions的实现

选项校验和规范化

mergeOptions函数的定义是在/src/core/util/options.js文件当中,部分代码如下:

 export function mergeOptions (
   parent: Object,// 选项
   child: Object, // 选项
   vm?: Component // Vue 实例
 ): Object {
   // * 1. 校验选项中的 components 里的名称是否合法。
   if (process.env.NODE_ENV !== 'production') {
     checkComponents(child)
   }
   
   // * ['type1'], { type2: { type: String, default: '' } } 两种形式
   // * 2. 都转换成后一种形式
   normalizeProps(child, vm)
   
   // * ['injectKey1'], { injectKey2: { from: 'xxx', default: 'yyy' } } 两种形式
   // * 3. 都转换成后一种形式
   normalizeInject(child, vm)
   
   // * directive 有两种形式 function() {} 或者是 { bind, update, ... }
   // * 4. 都转换成后一种形式
   normalizeDirectives(child)
 ​
   // * 合并策略
   ...
 }

在正式合并之前,会优先校验components里的组件名称是否合法,如果不合法会进行提示。

 function checkComponents (options: Object) {
   for (const key in options.components) {
     validateComponentName(key)
   }
 }
 ​
 // 校验组件名称是否合法
 export function validateComponentName (name: string) {
   // 1. 判断组件名是否合法,如数字开头的则不合法
   if (!new RegExp(`^[a-zA-Z][\-\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
     warn(
       'Invalid component name: "' + name + '". Component names ' +
       'should conform to valid custom element name in html5 specification.'
     )
   }
   // 2. 判断组件是否是自身定义的组件名,如 slot 等。
   // 3. 判断组件是否是 html 中的标签名,如 div 等
   if (isBuiltInTag(name) || config.isReservedTag(name)) {
     warn(
       'Do not use built-in or reserved HTML elements as component ' +
       'id: ' + name
     )
   }
 }

除此之外, Vue还做了以下几点处理。通过选项形式的转换,将多种写法的选项转换成统一形式:

  1. normalizeProps方法:处理props,将数组形式定义的props转换成对象形式。
  2. normalizeInject方法:处理inject,将数组形式定义的inject转换成对象形式。
  1. normalizeDirectives方法:处理directives,将指令数组里函数形式定义的directive转换成对象形式。

处理前和处理后对比结果如下:

 // ===> 处理前 <===
 {
   props: ['user-name'],
   inject: ['id'],
   directives: [function add() {}]
 }
 ​
 // ===> 处理后 <====
 {
   // 对象形式
   props: {
     userName: { // 转换成驼峰命名
       type: null
     }
   },
   // 对象形式
   inject: {
     id: {
       from: 'id'
     }
   },
   directives: [{
     // 对象形式
     bind: function add() {}
     update: function add() {}
   }]
 }

在校验完成之后,接下里就是正式的合并流程了,Vue针对每个规定的配置选项都有定义好的合并策略,例如data,component,mounted,methods等。如果Vue父子选项配置具有相应的选项,那么直接按照相应的合并策略进行合并。合并的入口如下:

 export function mergeOptions (
   parent: Object,
   child: Object,
   vm?: Component
 ): Object {
   ...
   // * 合并策略
   const options = {}
   let key
   for (key in parent) {
     mergeField(key)
   }
   for (key in child) {
     if (!hasOwn(parent, key)) {
       mergeField(key)
     }
   }
   function mergeField (key) {
     // 根据 key 获取相应的合并策略
     const strat = strats[key] || defaultStrat
     // 用相应的合并策略进行合并
     options[key] = strat(parent[key], child[key], vm, key)
   }
   return options
 }

以上通过两个for循环,遍历parentchildkeykey这里指的是data/methods/created等),然后依次调用mergeField方法。mergeField则是通过keystrats中找到对应的合并策略,然后用该合并策略进行相应合并。如果找不到合并策略,则使用默认合并策略defaultStrat

这里的strats已经在该文件中定义,现在重点来看一下Vuestrats是如何定义合并策略的。

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)
   }
 ​
   return mergeDataOrFn(parentVal, childVal, vm)
 }

可以看出当vm不存在时,如果childValdata不为函数形式,那么在非开发环境下就会报错,这也是为什么我们平时在写组件data时需要写成函数形式的原因。

但是这里的vm在什么情况下不存在呢?我们可以全局搜索一下mergeOptions(,看看哪些位置调用了该方法:

img

可以看出,在extendmixin中,由于处理构造函数阶段时,是没有实例的,所以也就不会传vm。这里我们主要讨论extend

接下来我们全局搜索一下extend在哪些地方被调用了。

img

可以看到,extend方法主要在两个位置被调用。

第一个位置在 src/core/global-api/assets.js文件

 ASSET_TYPES.forEach(type => {
     Vue[type] = function (
       id: string,
       definition: Function | Object
     ): Function | Object | void {
         ...
         if (type === 'component' && isPlainObject(definition)) {
           definition.name = definition.name || id
           // * 通过继承,返回新的构造函数(相当于 子组件 的构造函数)
           definition = this.options._base.extend(definition)
         }
         ...
         // * component / directive / filter 
         // * 将注册的内容全部添加到 Vue 构造函数的 options 上
         this.options[type + 's'][id] = definition
         return definition
     }
   })

通过遍历ASSET_TYPES,在Vue构造函数上添加了component静态属性,即当我们使用Vue.component的时候,实际上会执行这里的this.options._base.extend(definition),即调用了extend方法来将传入的组件选项合并后返回新的构造函数。

第二个位置在src/core/vdom/create-component.js文件

 export function createComponent (
   Ctor: Class<Component> | Function | Object | void,
   data: ?VNodeData,
   context: Component,
   children: ?Array<VNode>,
   tag?: string
 ): VNode | Array<VNode> | void {
   ...
   // * 这里的 Ctor 有几种形式
   // * 1. 全局形式定义的 component,那么 Ctor 是构造函数形式
   // * 2. 局部定义的 component,那么是 对象形式。会对对象形式进行 extend 处理
   const baseCtor = context.$options._base
 ​
   // 对象形式。会对对象形式进行 extend 处理
   if (isObject(Ctor)) {
     Ctor = baseCtor.extend(Ctor)
   }
   ...
   return vnode
 }

这段代码在patch阶段执行的(仅做了解)。传入的Ctor一种情况是全局定义的组件,此时Ctor通过extend创建,传入的是构造函数形式。另外一种情况则是局部注册的组件,传入的是选项配置形式,此时会执行baseCtor.extend(Ctor),同样会通过extend来创建构造函数。

因此,无论是全局注册的组件还是局部组件,最终都会调用extend方法,而extend方法在合并选项的时候会校验传入的data是否是函数形式,这也就是为什么在定义组件时data必须是以函数形式定义。

好了,了解完了data在组件中为什么要为函数形式后,我们继续看data的后续合并过程。mergeDataOrFn函数执行时最终调用的都是mergeData函数:

 function mergeData (to: Object, from: ?Object): Object {
   if (!from) return to
   let key, toVal, fromVal
 ​
   const keys = hasSymbol
     ? Reflect.ownKeys(from)
     : Object.keys(from)
 ​
   for (let i = 0; i < keys.length; i++) {
     key = keys[i]
     // key 为 __ob__ 则跳过 
     if (key === '__ob__') continue
     toVal = to[key]
     fromVal = from[key]
     // * 自身不存在这个key,那么使用将 from 的 key 和 value 添加到 to 上
     // * 如果 to 原本是响应式的,那么新增的 key 值也需要是响应式的
     if (!hasOwn(to, key)) {
       set(to, key, fromVal)
     } else if (
       toVal !== fromVal &&
       isPlainObject(toVal) &&
       isPlainObject(fromVal)
     ) {
       // * 如果都是对象,继续合并
       mergeData(toVal, fromVal)
     }
   }
   return to
 }

这里的mergeData比较简单,实际上就是递归将两个对象合并。需要注意的是,在合并的过程中,如果data是响应式的,那么合并后添加的属性也需要是响应式的。

生命周期合并

生命周期的钩子是在src/shared/constant.js中定义

 export const LIFECYCLE_HOOKS = [
   'beforeCreate',
   'created',
   'beforeMount',
   'mounted',
   'beforeUpdate',
   'updated',
   'beforeDestroy',
   'destroyed',
   'activated',
   'deactivated',
   'errorCaptured',
   'serverPrefetch'
 ]

mergeHook是生命周期钩子合并的策略,其核心是将父选项和子选项的对应生命周期合并成数组形式,如果存在相同的生命周期执行函数,那么会进行去重处理。

 function mergeHook (
   parentVal: ?Array<Function>,
   childVal: ?Function | ?Array<Function>
 ): ?Array<Function> {
   const res = childVal
     ? parentVal
       // 都存在时,拼接数组
       ? parentVal.concat(childVal)
       : Array.isArray(childVal)
         // parent 不存在时
         ? childVal
         : [childVal]
     // child 不存在,使用 parent
     : parentVal
   return res
     ? dedupeHooks(res)
     : res
 }
 ​
 // hooks 去重
 function dedupeHooks (hooks) {
   const res = []
   for (let i = 0; i < hooks.length; i++) {
     if (res.indexOf(hooks[i]) === -1) {
       res.push(hooks[i])
     }
   }
   return res
 }
 ​
 LIFECYCLE_HOOKS.forEach(hook => {
   strats[hook] = mergeHook
 })

下面结合具体例子看看实际合并的结果:

     const extend = {
       created() {
         console.log('extends')
       }
     }
     const mixins = {
       created() {
         console.log('mixins')
       }
     }
   
     // 父构造函数
     const Parent = Vue.extend({
       created() {
         console.log('parent created')
       },
       mixins: [mixins],
       extends: extend,
     })
 ​
     // 子构造函数
     const Child = Parent.extend({
       created() {
         console.log('child')
       },
       mixins: [mixins],
       extends: {
         created() {
           console.log('child extends')
         }
       }
     })
 ​
     new Child()
     // extends
     // mixins
     // parent created
     // child extends
     // child

由于mixins里的created在合并时去重了,所以只会打印一遍mixins。另外可以看出,生命周期在执行时,parentextends/mixins里的生命周期都是优先于child生命周期执行的

components/filters/directives合并

 function mergeAssets (
   parentVal: ?Object,
   childVal: ?Object,
   vm?: Component,
   key: string
 ): Object {
   const res = Object.create(parentVal || null)
   if (childVal) {
     process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
     return extend(res, childVal)
   } else {
     return res
   }
 }
 ​
 ASSET_TYPES.forEach(function (type) {
   strats[type + 's'] = mergeAssets
 })

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

这合并资源选项的时候,首先会创建一个原型指向父选项的空对象,再将子选项赋值给空对象。注意这里的父选项是通过原型链访问,而子选项是直接添加到对象上的。例如:

 Vue.component('test', {})
   const vm = new Vue({
     components: {
       test: 'test'
     }
   })
 console.log('vm.$options ==> ', vm.$options);
 // 合并后,父类的 options 通过 __proto__ 访问
 { 
   components: {
     test: "test",
     __proto__: {
       KeepAlive: { ... },
       Transition: { ... },
       TransitionGroup: { ... },
       test: ...
     }
   },
   directives: {},
   filters: {},
   _base: ...
 }

这里的__proto__指向的就是复习选项的components

watch合并

watch的策略是:

  • 当子选项不存在时,使用父选项;
  • 当父选项不存在时,使用子选项;
  • 当父选项和子选项都存在时,如果他们具有相同的观测字段,那么将其合并成数组形式。
 strats.watch = function (
   parentVal: ?Object,
   childVal: ?Object,
   vm?: Component,
   key: string
 ): ?Object {
   ...
   // 子类不存在,使用父类
   if (!childVal) return Object.create(parentVal || null)
   ...
   // 父类不存在,使用子类
   if (!parentVal) return childVal
   const ret = {}
   extend(ret, parentVal)
   for (const key in childVal) {
     let parent = ret[key]
     const child = childVal[key]
     // 如果父类存在,改写成数组形式
     if (parent && !Array.isArray(parent)) {
       parent = [parent]
     }
     // 拼接父类和子类
     ret[key] = parent
       ? parent.concat(child)
       : Array.isArray(child) ? child : [child]
   }
   return ret
 }

props,methods,inject,computed合并

这一类的选项合并比较简单:

  • 当父选项不存在时,使用子选项;
  • 当子选项不存在时,使用父选项;
  • 当两者都存在时,使用子选项覆盖父选项。
 strats.props =
 strats.methods =
 strats.inject =
 strats.computed = function (
   parentVal: ?Object,
   childVal: ?Object,
   vm?: Component,
   key: string
 ): ?Object {
   if (childVal && process.env.NODE_ENV !== 'production') {
     assertObjectType(key, childVal, vm)
   }
   if (!parentVal) return childVal
   // 创建空对象
   const ret = Object.create(null)
   extend(ret, parentVal)
   if (childVal) extend(ret, childVal)
   return ret
 }

provide合并

使用的是mergeDataOrFn合并策略,同data合并。

总结

到这里我们就已经对所有的合并策略都有所了解了。总结一下就是

  1. data,provide,props,methods,inject,computed,``components,filters,directives基本都是在父子选项同时存在的情况下,子覆盖父。
  2. 生命周期在父子选项同时存在的情况下,会合并成数组形式,且去重。
  1. watch在父子选项同时存在的情况下,会合并成数组形式,不去重。

Vue.extend的实现

Vue.extend的定义是在core/gloabl-api/extend.js文件里面,主要用于通过选项配参数生成新的构造函数。这里的参数extendOptions就是我们在定义组件时传入的配置选项。

 Vue.extend = function (extendOptions: Object): Function {
     extendOptions = extendOptions || {}
     const Super = this
     const SuperId = Super.cid
     const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
     // * 1. 查看配置选项中是否缓存有构造函数
     if (cachedCtors[SuperId]) {
       return cachedCtors[SuperId]
     }
 ​
     // * 2. 校验组件名称
     const name = extendOptions.name || Super.options.name
     if (process.env.NODE_ENV !== 'production' && name) {
       validateComponentName(name)
     }
 ​
     // * 3. 继承
     const Sub = function VueComponent (options) {
       this._init(options)
     }
     Sub.prototype = Object.create(Super.prototype)
     Sub.prototype.constructor = Sub
     Sub.cid = cid++
   
     ...
   }

我们先看前半段代码,前半段主要做了三件事:

  1. 检验是否通过该选项配置生成过相应构造函数,如果生成过,那么直接使用生成的构造函数即可。这里相当于做了一层优化。
  2. 校验组件名称是否合法
  1. 通过原型实现继承,生成新的构造函数

接下来就是选项合并了

     // * 4. 合并选项
     Sub.options = mergeOptions(
       Super.options,
       extendOptions
     )

参数Super.options就是Vue.options,前面已经提过两次,这里不再赘述了。这里合并后相当于将Vue.options扩充了,并将扩充后的结果保存到Sub.options上(即新的构造函数options上),所以在通过该构造函数实例化的时候,拥有extendOptions配置的相关功能。

最后,会在新生成的构造函数上添加一些静态方法和属性。注意这里的superOptioins/extendOptions/sealedOptioins都在resolveConstructorOptions方法寻找options中使用到。

     // * 5. 添加 super
     Sub['super'] = Super
       
     ...
 ​
     // * 6. 添加一些方法
     // allow further extension/mixin/plugin usage
     Sub.extend = Super.extend
     Sub.mixin = Super.mixin
     Sub.use = Super.use
     ASSET_TYPES.forEach(function (type) {
       Sub[type] = Super[type]
     })
     if (name) {
       Sub.options.components[name] = Sub
     }
 ​
     // * 7. 添加一些属性
     // * 父选项
     Sub.superOptions = Super.options
     // * 传入的配置选项
     Sub.extendOptions = extendOptions
     // * 合并后的配置相许那个
     Sub.sealedOptions = extend({}, Sub.options)

最后总结来讲,Vue.extend方法实际上就是通过原型继承,并将Vue.optionsextendOptions合并,从而实现一个新的构造函数。

Vue.mixin的实现

 Vue.mixin`方法的实现更是简单,打开`core/global-api/mixin.js
 Vue.mixin = function (mixin: Object) {
     this.options = mergeOptions(this.options, mixin)
     return this
   }

不难看出,mixin实际上就是将两个选项配置进行合并。

结尾

在结尾之前,这里再抛出一个问题,在Vue实例化过程中有一层判断,当_isComponenttrue时,将不会进行选项合并,这是为什么呢?

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

这是因为当_isComponenttrue时,此时为组件渲染阶段,options的内容为

 const options: InternalComponentOptions = {
   _isComponent: true,
   _parentVnode: vnode,
   parent
 }

此时并没有需要选项合并的项,所以也就没有必要mergeOptions了。

至此,选项合并的实现与应用我们已经学完了,下一章节将会学习Vue的核心模块之一:响应式系统。

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: qgh 原文链接:https://juejin.im/post/6986888911385526308

回到顶部