Bootstrap

人人都能懂的Vue源码系列(三)—resolveConstructorOptions函数

上篇文章介绍了Vue构造函数的部分实现,如果当前Vue实例不是组件,则执行mergeOptions方法。

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)
复制代码

关于mergeOptions方法,我们之后的博文会做详细介绍。今天主要来研究resolveConstructorOptions,从字面意思来看,它是来解析constructor上options属性的,具体来看源码。

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有super属性,说明Ctor是Vue.extend构建的子类
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions // Vue构造函数上的options,如directives,filters,....
    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
}
复制代码

这个方法要分成两种情况来说明,第一种是Ctor是基础Vue构造器的情况,另一种是Ctor是通过Vue.extend方法扩展的情况。

Ctor是基础Vue构造器

当Ctor(Ctor其实就是构造函数)是基础Vue构造器时,如通过new关键字来新建Vue构造函数的实例

const vm = new Vue({
  el: '#app',
    data: {
      message: 'Hello Chris'
    }
})
复制代码

这个时候options就是Vue构造函数上的options。如下图

那么这个options是在哪里定义的呢?在之前的代码中好像没有看到options的定义在哪里?此时我们应该怎么去找这个options定义的地方呢?

这里教大家一个方法,首先找到package.json,在这里可以找到我们平时用到的一些npm脚本。以npm run dev为例。实际上npm run dev是执行了下列的命令 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"

rollup是类似于webpack的打包工具。我们可以看到这条命令指向了一个地址scripts/config,之后还指定了一个Target。找到script/config,发现这个文件里有TARGET为web-full-dev的配置。

// Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  }
复制代码

来分析上面的代码,入口文件的地址在web/entry-runtime-with-compiler.js。这个文件就是对Vue构造函数进行的第一层包装了。由于今天分析的是options相关的内容,而这层包装里没有options相关的内容,所以这个文件我们不展开讲(之后有文章会详细介绍)。但是注意这里的代码

...
import Vue from './runtime/index'
...
复制代码

我们Vue构造函数的第二层包装,就在这个文件里了。忽略其他的代码,我们来看关于Vue.options的部分

...
import Vue from 'core/index' // 第三层包装
import platformDirectives from './directives/index'
import platformComponents from './components/index'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
...

// platformDirectives相关
// 这里导出Vue全局指令model,show
import model from './model'
import show from './show'
export default {
  model,
  show
}

// platformComponents相关
// 这里导出Vue全局组件Transition,TransitionGroup
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
  Transition,
  TransitionGroup
}
复制代码

上面的代码主要是给Vue.options.directives添加model,show属性,给Vue.options.components添加Transition,TransitionGroup属性。那么还有filters,_base属性,以及components中的KeepAlive又是怎么来的呢?

这就要看Vue的第三层包装里都做了些什么?找到core/index,同样我们只看Vue.options相关代码。

mport Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue)
...
复制代码

instance/index 就是我们第二篇文章——构造函数定义的那个文件。这个文件我们之前看过,没有和Vue构造函数options相关的代码。那么我们剩下的没有配置的options一定是在initGlobalAPI上配置了。接来下看看/global-api/index的代码。

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
...
export function initGlobalAPI (Vue: GlobalAPI) {
  ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  Vue.options._base = Vue
  extend(Vue.options.components, builtInComponents)
  ...
}

// shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

// core/components/index
import KeepAlive from './keep-alive'
export default {
  KeepAlive
}
复制代码

至此,filters,_base和components中的KeepAlive就都有了。通过这三层包装,Vue构造函数的options对象就油然而生,看这些文字可能有点绕,我们直接上图。

回到resolveConstructorOptions的源码中,当Ctor.super不存在时,直接返回基础构造器的options。即上图经过两次包装的options。那么Ctor.super是什么呢?

Ctor.super是通过Vue.extend构造子类的时候。Vue.extend方法会为Ctor添加一个super属性,指向其父类构造器。

Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub['super'] = Super
  ...
}
复制代码

所以当Ctor时基础构造器的时候,resolveConstructorOptions方法返回基础构造器的options。

Ctor是Vue.extend创建的"子类"

Vue.extend方法我们之后的博文再进行详细介绍,这里大家可以先把Vue.extend的功能笼统的理解为继承。我们接下来看resolveConstructorOptions相关的代码,如果Ctor是Vue.extend创建的"子类",那么在extend的过程中,Ctor对象上就会有super属性。

Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub['super'] = Super
  ...
}
复制代码

如果有super属性,就会去执行if块内的代码

...
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
...
// Vue.extend相关代码
Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub.superOptions = Super.options // Sub.superOptions指向基础构造器的options
  ...
}
复制代码

首先递归调用resolveConstructorOptions方法,返回"父类"上的options并赋值给superOptions变量。然后把"自身"的options赋值给cachedSuperOptions变量。

然后比较这两个变量的值,当这两个变量值不等时,说明"父类"的options改变过了。例如执行了Vue.mixin方法,这时候就需要把"自身"的superOptions属性替换成最新的,之后检查是否"自身"的options是否发生变化?resolveModifiedOptions的功能就是这个。

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

说了这么多,大家可能还是有点陌生,我们直接举个例子来说明一下。

  var Profile = Vue.extend({
     template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>'
  })
  Vue.mixin({ data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
   }})
   new Profile().$mount('#example')
复制代码

由于Vue.mixin改变了"父类"options,源码中superOptions和cachedSuperOptions就不相等了,大家可以去jsfiddle试试效果。 接下来看看resolveModifiedOptions都干了哪些事情?

function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified // 定义modified变量
  const latest = Ctor.options // 自身的options
  const extended = Ctor.extendOptions // 构造"自身"时传入的options
  const sealed = Ctor.sealedOptions // 执行Vue.extend时封装的"自身"options,这个属性就是方便检查"自身"的options有没有变化
 // 遍历当前构造器上的options属性,如果在"自身"封装的options里没有,则证明是新添加的。执行if内的语句。调用dedupe方法,最终返回modified变量(即”自身新添加的options“)
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = dedupe(latest[key], extended[key], sealed[key])
    }
  }
  return modified
}
复制代码

那么dedupe方法又是怎么处理的呢?

function dedupe (latest, extended, sealed) {
  // compare latest and sealed to ensure lifecycle hooks won't be duplicated
  // between merges
  if (Array.isArray(latest)) {
    const res = []
    sealed = Array.isArray(sealed) ? sealed : [sealed]
    extended = Array.isArray(extended) ? extended : [extended]
    for (let i = 0; i < latest.length; i++) {
      // push original options and not sealed options to exclude duplicated options
      if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
        res.push(latest[i])
      }
    }
    return res
  } else {
    return latest
  }
}
复制代码

从作者的注释可以看到这个方法主要就是防止生命周期构造函数重复。我们来看该方法传入的3个参数: latest,extended,sealed

  1. lateset表示的是"自身"新增的options。
  2. extended表示的是当前构造器上新增的extended
  3. options,sealed表示的是当前构造器上新增的封装options。

回到源码,如果latest不是数组的话(lateset是"自身"新增的options),这里不需要去重,直接返回latest。

如果传入的latest是数组(如果latest是数组,一般这个新增的options就是生命周期钩子函数),则遍历该数组,如果该数组的某项在extended数组中有或者在sealed数组中没有,则推送到返回数组中从而实现去重。(这个去重逻辑目前自己还不是特别明白,之后如果明白了会在这里更新,有明白的同学们请在评论区留言)

现在我们了解了resolveModifiedOptions和dedupe方法的作用,接下来回到resolveConstructorOptions源码。

  if (modifiedOptions) {
    extend(Ctor.extendOptions, modifiedOptions)
  }
  options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
  if (options.name) {
    options.components[options.name] = Ctor
  }
复制代码

如果”自身“有新添加的options,则把新添加的options属性添加到Ctor.extendOptions属性上,再调用mergeOptions方法合并"父类"构造器上的options和”自身“上的extendOptions(mergeOptions在下一篇博文中介绍),最后返回合并后的options。

看到这里,可能会感觉到头晕,为了让大家更好的理解。我画了下面的流程图。

关于resolveConstructorOptions的内容我们已经解析完了,下篇博客主要讲mergeOptions方法,它在整个Vue中属于比较核心的一个方法。敬请期待!

如果您觉得我的文章还不错,请不要吝惜你们的点赞!谢谢!

;