Vue 2.0 源码解读系列(二) - 打开 Vue 神秘礼盒之合并选项(2)

avatar

鄢栋,微医前端技术部前端工程师。有志成为一名全栈开发工程师甚至架构师,路漫漫,吾求索。 生活中通过健身释放压力,思考问题。

文章篇幅较长, 建议花整块时间阅读分析。 另外由于篇幅过长, 本文分三篇文章产出, 便于大家理解与阅读。

mergeOptions 函数

上一篇文章分析中,我们分析完了 mergeOptions 函数的第一个参数 resolveConstructorOptions, 我们再回头看看 mergeOptions 这个函数。

作用:把构造函数上的 options 和实例化时传入的 options 进行合并操作并生成一个新的 options

从上述分析可知,mergeOptions 函数是要将实例构造器上的 options, 实例化时传入的 options, 以及当前实例合并成一个 options, 看一下 mergeOptions 函数的定义:

export function mergeOptions (

  parent: Object,  // 实例构造器上的 options

  child: Object, // 实例化时传入的 options

  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)

  // 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

  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

}

  • 1、checkComponents(child)校验组件的名称是否合法
  • 2、如果 child(传入的 options)是 function 类型的话,我们取其 options 属性作为 child
  • 3、将 props, inject, directives 转化成对象形式(normalizeProps(child, vm)normalizeInject(child, vm)normalizeDirectives(child)

第一条和第 2 条我们就不做细致分析了, 有兴趣的可自行查阅。组件名称,我们在使用过程中不要过分特殊就行 O(∩_∩)O~

normalizeProps

我们来看看,vue 是如何将 props 转换的。首先需要了解 props 的用法,我们一般有两种, 一种是数组形式, 一种是对象形式。

// 数组形式

Vue.component('test-component', {

  props: ['testData'],

  template: '<span>{{ testData }}</span>'

})

// 对象形式

Vue.component('test-component', {

  props: {

    testData: {

      type: String,

      default: ''

    }

  },

  template: '<span>{{ testData }}</span>'

})

再看看 normalizeProps 函数的定义

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

}

这段代码,现实声明了 res 这个变量, 后边用来存储处理后的对象, 最后赋值到 options.props 上。

  • 如果 props 是数组的形式,就遍历字符串数组,给每一个 key 赋值为{type: null}

也就是数组形式的会被转为:

{

    testData: { type: null }

}

  • 如果 props 是对象形式, 则同样是先把 key 值驼峰化, 然后判断 key 对应的 value 是不是纯对象, 是就直接赋值给 res 返回;不是就把{type: value}赋值给 res, 所以对象形式的 props 最终会被转为:
{

    testData: {

      type: String,

      default: ''

    }

}

normalizeInject

/**

* Normalize all injections into Object-based format

*/

function normalizeInject (options: Object, vm: ?Component) {

  const inject = options.inject

  if (!inject) return

  const normalized = options.inject = {}

  if (Array.isArray(inject)) {

    for (let i = 0; i < inject.length; i++) {

      normalized[inject[i]] = { from: inject[i] }

    }

  } else if (isPlainObject(inject)) {

    for (const key in inject) {

      const val = inject[key]

      normalized[key] = isPlainObject(val)

        ? extend({ from: key }, val)

        : { from: val }

    }

  } else if (process.env.NODE_ENV !== 'production') {

    warn(

      `Invalid value for option "inject": expected an Array or an Object, ` +

      `but got ${toRawType(inject)}.`,

      vm

    )

  }

}

同样也是分数组和对象形式:

// 父级组件提供 'foo'

var Provider = {

  provide: {

    foo: 'bar'

  },

  // ...

}

// 子组件数组形式注入 'foo'

var Child = {

  inject: ['foo'],

  created () {

    console.log(this.foo) // => "bar"

  }

  // ...

}

// 子组件对象形式注入 'foo'

const Child = {

  inject: {

    foo: {

      from: 'bar',

      default: 'foo'

    }

  }

}

最终经过 normalizeInject 处理后都会变成:

// array

{

  foo: { from: 'foo'}

}

// object

{

foo: {

  from: 'bar',

  default: 'foo'

}

}

normalizeDirectives

/**

* Normalize raw function directives into object format.

*/

function normalizeDirectives (options: Object) {

  const dirs = options.directives

  if (dirs) {

    for (const key in dirs) {

      const def = dirs[key]

      if (typeof def === 'function') {

        dirs[key] = { bind: def, update: def }

      }

    }

  }

}

directives 分函数和对象两种写法:

// 对象形式注册

Vue.directive('my-directive', {

  bind: function () {},

  inserted: function () {},

  update: function () {},

  componentUpdated: function () {},

  unbind: function () {}

})

// 函数注册 (指令函数)

Vue.directive('my-directive', function () {

  // 这里将会被 `bind` 和 `update` 调用

})

其中, 源码中会将函数形式的指令处理为 bind 和 update 调用, 也就是函数形式的最终会被处理为:

{

  'my-directive': {

    bind: function () {},

    update: function () {}

  }

}

我们再接着看 mergeOptions 下面的代码:

    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)

      }

    }

当传入的 options 里(child)有 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 做合并处理。

到此, 本篇结束。讲了 mergeOptions 函数里的以下内容:

  • props 标准化为对象形式 {type: null}或{type: val}
  • inject 标准化为对象形式 {from: val}
  • directives 标准化bind, update 函数
  • 传入选项的 extends, mixins 属性的合并

小结

下一篇文章我们会进入到 mergeOptions 函数的核心代码部分: 合并策略。

终于熬完了合并选项的分析(三篇文章), 每天抽出睡觉的时间来啃这块, 非常不容易,促进自己成长, 希望也能给小可爱们带去一些启发, 如果有地方觉得说的不清楚的欢迎在下方评论区提问,讨论~

看到这里的小可爱们, 可顺手点个赞鼓励我继续创作, 创作不易,一起学习, 早日实现财富自由~

参考资料

推荐阅读