人人都能懂的Vue源码系列(四)—mergeOptions

上篇文章中咱们讲了resolveConstructorOptions,它的主要功能是解析当前实例构造函数上的options,不太明白的同窗们能够看本系列的前几篇文章。在解析完构造函数上的options以后,须要把构造函数上的options和实例化时传入的options进行合并操做,并生成一个新的options。html

这个合并操做就是今天要讲的mergeOptions。若是你们不想看枯燥的讲解,能够翻到文章最后,查看整个mergeOptions的流程图。vue

Merge two option objects into a new one. Core utility used in both instantiation and inheritance.数组

先来看源码中对mergeOptions方法的注释。mergeOptions的功能是合并两个options对象,并生成一个新的对象,是实例化和继承中使用的核心方法。既然这么重要,那我就带你们一行一行的来解析代码,让你们看完这篇文章后,能基本上理解mergeOptions。bash

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)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, 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)
  }
  return options
}
复制代码

首先看传入的三个参数,parent,child,vm,这三个参数分别表明的是该实例构造函数上的options,实例化时传入的options,vm实例自己。结合Vue做者写的注释,咱们明白了,原来mergeoptions方法是要合并构造函数和传入的options这两个对象。接下来往下看app

if (process.env.NODE_ENV !== 'production') {
    checkComponents(child) // 检查组件名称是否合法
 }
复制代码

这段代码主要是判断当前环境是否是生产环境,若是不是,则调用checkComponents方法来检查组件名称是不是可用名称?咱们来看看checkComponents的逻辑ide

function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}
export function validateComponentName (name: string) {
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}
复制代码

若是child的options(实例化传入的options)有components属性,以下面这种状况svg

const app = new Vue({
   el: '#app',
   ...
   components: {
     childComponent
   }
   ...
})
复制代码

那么就调用validateComponentName来验证传入的组件名称是否符合如下特征函数

  1. 包含数字,字母,下划线,链接符,而且以字母开头
  2. 是否和html标签名称或svg标签名称相同
  3. 是否和关键字名称相同,如undefined, infinity等

若是知足第一条,而且第2,3条都是不相同的话,那么组件名称可用。 咱们再回到mergeOptions源码中post

if (typeof child === 'function') {
    child = child.options
}
复制代码

若是child是function类型的话,取其options属性做为child。 接下来看这三个以normalize开头的方法ui

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
复制代码

这三个方法的功能相似,分别是把options中props,inject,directives属性转换成对象的形式。有时候传入的是数组,以下面这种状况

Vue.component('blog-post', {
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
复制代码

normalizeProps

咱们先来看props处理的逻辑

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}
复制代码

首先明确这两个方法里的参数是什么,options传入的是child,即实例化时传入的options,而vm是实例。继续看源码

const props = options.props
if (!props) return
const res = {}
let i, val, name
复制代码

上面的代码主要是声明一些变量。res用来存放修改后的props,最后把res赋给新的props。下面的逻辑能够分为两种状况来考虑

props是数组

当props是数组的时候,以下面这种状况

Vue.component('blog-post', {
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
复制代码

它的处理逻辑是:遍历props数组,把数组的每一项的值做为res对象的key,value值等于{type: null}。即把上面例子中的['postTitle']转换成下面这种形式

{
  postTitle: { type: null }
}
复制代码

props是对象

当props是对象时,以下面这种状况

Vue.component('my-component', {
  props: {
    // 必填的字符串
    propC: {
      type: String,
      required: true
    }
  }
})
复制代码

这种状况的处理逻辑是遍历对象,先把对象的key值转换成驼峰的形式。而后再判断对象的值,若是是纯对象(即调用object.prototype.toString方法的结果是[object Object]),则直接把对象的值赋值给res,若是不是,则把{ type: 对象的值}赋给res。最终会转换成

{
  propC: {
   type: String,
   required: true
  }
}
复制代码

若是传入的props不是纯对象也不是数组,且当前环境也不是生产环境,则抛出警告。

warn(
  `Invalid value for option "props": expected an Array or an Object, ` +
  `but got ${toRawType(props)}.`,
   vm
)
复制代码

最后,把处理过的props从新赋值给options.props。

normalizeInject

这个方法的逻辑和normalizeProps相似,主要是处理inject。inject属性若是你们平时不是写库或者插件的话,可能不多接触到,能够点击inject的使用查看。inject的传入和props相似,能够传入object,也能够传入array

// array
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}
// object
const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: 'foo'
    }
  }
}
复制代码

因为这个方法和normalizeProps逻辑基本同样,这里也不具体展开讲了。上面的demo最终会被转换成以下形式

// array
{
  foo: { from: 'foo'}
}
// object
{
 foo: {
   from: 'bar',
   default: 'foo'
 }
}
复制代码

normalizeDirectives

这个方法主要是处理一些自定义指令,若是不了解自定义指令的同窗能够点击自定义指令查看。这里的方法处理逻辑主要针对自定义指令中函数简写的状况。以下

Vue.directive('color', function (el, binding) {
  el.style.backgroundColor = binding.value
})
复制代码

normalizeDirectives构造函数会把这个指令传入的参数,最终转换成下面这种形式

color: {
    bind: function (el, binding) {
      el.style.backgroundColor = binding.value
    },
    update: function (el, binding) {
      el.style.backgroundColor = binding.value
    }
  }
复制代码

mergeOptions合并策略

讲完了三个以normalize开头的方法,咱们回到mergeOptions中继续往下看。

const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
复制代码

这段代码的处理的逻辑是,当传入的options里有mixin或者extends属性时,再次调用mergeOptions方法合并mixins和extends里的内容到实例的构造函数options上(即parent options)好比下面这种状况

const childComponent = Vue.component('child', {
      ...
      mixins: [myMixin],
      extends: myComponent
      ...
 })
 const myMixin = {
      created: function () {
        this.hello()
      },
      methods: {
        hello: function () {
          console.log('hello from mixin')
      }
    }
  }
 const myComponent = {
      mounted: function () {
        this.goodbye()
      },
      methods: {
        goodbye: function () {
          console.log('goodbye from mixin')
        }
     }
  }
复制代码

把传入的mounted, created钩子处理函数,还有methods方法和parent options作合并处理。继续看源码

const options = {}
let key
复制代码

变量options存储合并以后的options,变量key存储parent options和child options上的key值。接下来的部分算是mergeOptions方法的核心处理部分了,像炒菜同样,前面的代码至关于把全部的菜都配好了,接下来的部分就是教你怎么去炒菜了。

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)
  }
复制代码

前两段for循环代码大同小异,都是遍历options上的key值,而后调用mergeField方法来处理options。mergeField方法中出现了一个变量strats和defaultStrat,这两个变量存储的就是咱们的合并策略,也就是炒菜的菜谱,咱们先来看看defaultStrat

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
复制代码

defaultStrat的逻辑是,若是child上该属性值存在时,就取child上的属性值,若是不存在,则取parent上的属性值。

如今咱们知道默认的合并策略是什么了,接下来看其余的合并策略。咱们来看看strats里都有哪些属性?

strats合并策略
上图就是strats中全部的策略了,粗略看起来里面的内容很是的多,若是细细分析会发现,其实总结起来无非就是几种合并策略。

钩子函数的合并策略

全部关于钩子函数的策略,其实都是调用mergeHook方法。

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}
复制代码

mergeHook采用了一个很是骚的嵌套三元表达式来控制最后的返回值。下面咱们来解析这段三元表达式 (1) child options上不存在该属性,parent options上存在,则返回parent上的属性。

图片描述

(2)child和parent都存在该属性,则返回concat以后的属性

图片描述

(3)child上存在该属性,parent不存在,且child上的该属性是Array,则直接返回child上的该属性

图片描述

(4) child上存在该属性,parent不存在,且child上的该属性不是Array,则把该属性先转换成Array,再返回。

图片描述
上面就是钩子函数合并策略,结合图片看应该会比较清晰。

props/methods/inject/computed的策略

介绍完了钩子函数的合并策略,咱们接下来看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
}
复制代码

这个合并方法逻辑很简单,若是child options上这些属性存在,则先判断它们是否是对象。

  1. 若是parent options上没有该属性,则直接返回child options上的该属性。
  2. 若是parent options和child options都有,则合并parent options和child options并生成一个新的对象。(若是parent和child上有同名属性,合并后的以child options上的为准)

components/directives/filters的合并策略

components/directives/filters这几个属性的处理逻辑以下

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
  }
}
复制代码

这里的处理逻辑和上一种状况的相似,这里不作过多讲解。

data和provide的策略

data和provide的策略相对来讲复杂一些,咱们先来看代码

export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component ): ?Function {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}
复制代码

这个合并策略能够分红两种状况来考虑。

第一种状况,当前调用mergeOptions操做的是vm实例(调用new新建vue实例触发mergeOptions方法)

return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
复制代码

若是新建实例时传入的options上有data属性,则调用mergeData方法合并实例上的data属性和其构造函数options上的data属性(若是有的话)

第二种状况,当前调用mergeOptions操做的不是vm实例(即经过Vue.extend/Vue.component调用了mergeOptions方法)

if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  }
复制代码

在这种状况下,其处理逻辑也是相似的。若是当前实例options或者构造函数options上有一个没有data属性,则返回另外一个的data属性,若是二者都有,则一样调用mergeData方法处理合并。

既然这两种状况都调用了mergeData方法,那咱们就继续来看看mergeData的源码。

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal
  const keys = Object.keys(from)
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}
复制代码

mergeData的逻辑是,若是from对象中有to对象里没有的属性,则调用set方法,(这里的set就是Vue.$set,先能够简单理解为对象设置属性。以后会细讲)若是from和to中有相同的key值,且key对应的value是对象,则会递归调用mergeData方法,不然以to的值为准,最后返回to对象。

这里咱们就讲完了data的合并策略,返回mergeOptions代码里,在通过这几种合并策略合并options后,最终返回合并后的options。

return options
复制代码

总结

讲到这里,整个mergeOptions的流程也全讲完了。这个方法牵扯到的内容比较多,流程也比较复杂,为了你们更好的理解和记忆,我画了一张图来表达整个mergeOptions的过程。

mergeoptions的流程图
若是你们以为个人文章写的还行,请为我点赞,大家的承认是我最大的动力。
相关文章
相关标签/搜索