Vue源码之参数处理

237 阅读4分钟

因为工作原因,很长时间没有更新文章了,抱歉抱歉,大家多多理解,谢谢支持!

上篇文章我们对Vue最开始是在Vue的原型上挂载了一些方法或属性,比如_init,onon,watch等等,今天我会用一个实例开始我们今天的分析。

我们知道Vue是这样用的。

new Vue({
  el: '#app',
  data () {
    return {
      message: 123
    }
  },
  methods: {}
})

我们接着转到源码文件。

......
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    // Vue是一个构造函数,所以应该用new来实例化,这也是为什么CDN生产环境中要new的原因
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
......
export default Vue

这里我们看到源码将传进来的参数在_init方法中运行。

文件地址:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    ...

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    ...

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    ...

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

我们看到一直到vm._self = vm这句代码为止,_init就做了3件事情

  • 给每个组件加一个 uid,方便标识
  • 加一个 _isVue 用于判断是否为vue组件。
  • 合并 options

我们这里先不讨论 new Vue 和组件的参数处理,所以我们先分析 else 里边的逻辑。

可以看到,这里调用了一个 mergeOptions 方法来进行参数合并,接收 vue 构造函数上的参数,实例的参数,实例对象。

我们来看看这个方法都做了什么。

文件路径:src\core\instance\init.js

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super) 
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

我们可以打印 options 看看构造函数上的options都有一些什么。

{
  components: {KeepAlive: {…}, Transition: {…}, TransitionGroup: {…}, runoob: ƒ},
  directives: {model: {…}, show: {…}},
  filters: {},
  _base: ƒ Vue(options)
}

可以看到,这里就是放了一些指令,组件,以及筛选器相关的通用属性而已。不深入讨论,后边再来说,现在只要知道有这个就行。

接着往下走,因为当前 Ctor 是 Vue 的构造函数,所以不存在 super ,直接将 options 返回。

我们接着看 mergeOptions 这个函数,传入的是 Vue 的构造函数,子组件的 options,和 vue 实例。

文件路径:src\core\util\options.js

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  ...

  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
}

这里大致看一下, mergeOptions 函数也做了三件事:

  • 统一 props, inject, directive 的写法,因为这三个属性会有多种写法,就以 directive 为例,可以用 Vue.directive 注册全局指令,也可以用 directives: {} 注册局部组件,如果每种写法都做兼容,那么要考虑到的情况就有很多了,所以作者将不同的写法进行了格式统一,再来进行分析就好轻松了。
  • 递归子组件的 options ,直至所有 options 都挂载了构造函数的属性和方法。
  • 合并子组件和构造函数的字段。

截止到这里,字段合并的功能已经全部结束。返回到 initMixin 的 "vm._self = vm" 这一行,打印vm.$options看看

{
  components: {},
  data: ƒ mergedInstanceDataFn(),
  directives: {},
  filters: {},
  methods: {},
  _base: ƒ Vue(options)
}

我们看到,这里多了 components, directives, filters 这3个对象,我们大胆猜一下作者将为什么这三个字段加入到每个组件中。

我们首先回顾一个经典面试题:组件的data数据为什么必须要以函数返回的形式。

组件是可用的vue实例,每个组件可能被用在各种地方,但是不管怎么用,用了多少次,他们的数据、方法和属性应该是相互独立,互不影响的。

vue 的作者用工厂模式的方法给每个组件都挂载构造函数中的各种数据,vue 参数合并的函数就是实现了这样一个功能。