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

1,006 阅读3分钟

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

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

前言

上一篇文章分析中,我们分析了 Vue 的整体的运行机制, 其中, 我们知道 Vue 在初始化的时候调用了 init() 函数: init

在 instance.js 文件中, 调用了 initMixin()方法。然后我们找到 init.js 中, 对 initMixin 函数的定义, 中间有这么一段代码:

    // merge options
    if (options && options._isComponent) { // 如果当前 Vue 实例是组件,执行 initInternalComponent
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options) // 该方法主要就是为 vm.$options 添加一些属性
    } else {
      // 否则就是实例化 Vue 对象,调用 mergeOptions
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

这段代码是合并选项, 那么到底是如何进行合并选项 options 呢?

首先我们来看直接实例化 Vue, 也就是:

      // 否则就是实例化 Vue 对象,调用 mergeOptions
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )

可以看到, 调用了mergeOptions(参数 1, 参数 2, 参数 3)函数

  • 参数 1: resolveConstructorOptions(vm.constructor);
  • 参数 2:options || {};
  • 参数 3: vm

参数 2, 是我们创建 Vue 实例的时候传入的 options 参数 3, Vue 自身的实例

所以, 主要看参数 1: resolveConstructorOptions(vm.constructor)函数。

本篇幅就来介绍, vue 是如何解析当前实例构造器上的 options 的resolveConstructorOptions 函数。

resolveConstructorOptions

解析当前实例构造函数上的 options

/**
 * @param {*} Ctor: vm.constructor
 * 这个方法要分成两种情况来说明
 * 第一种是 Ctor 是基础 Vue 构造器的情况
 * 另一种是 Ctor 是通过 Vue.extend 方法扩展的情况。
 */
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
    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
}

new Vue()构造子类

我们先看 Ctor 是基础 Vue 构造器的情况, 即通过 new Vue()构造子类
export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有 super 属性,说明 Ctor 是 Vue.extend 构建的子类
  ...
  return options
}

Ctor 其实就是 Vue 构造器,Ctor.options 就是 Vue 构造函数上的 options, 这个时候直接返回了。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>new Vue</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    {{message}}
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      },
      created: function (){
        console.log('this.constructor.options ', this.constructor.options)
      }
    })
  </script>
</body>
</html>

我们打印 Vue 构造器上的 options 对象: vm.constructor.options

可以看到基础构造器上有components, directives, filters, _base四个基础属性, 这些属性是从哪里定义的呢?

既然我们是初始化整个框架,那么一定时需要框架项目能够运行, 因此我们找到 package.json 文件, 找到运行命令 scripts: "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",

然后定位到 scripts/config.js, 找到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 在 platforms/web/entry-runtime-with-compiler.js 中, 我们看到这么一行代码: import Vue from './runtime/index'

在该文件中:

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)

关于 Vue.options 的设置就是这么几行代码, 然后我们分别代开 platformDirectives、platformComponents 文件 directives platformDirectives 文件导出了 Vue 的全局指令 v-model, v-show

components platformComponents 文件导出了 Vue 的全局动画组件 Transition, TransitionGroup 组件。

结合上面我们打印出的 vm.constructor.options 的截图 vm.constructor.options

我们看到,除了上面两个文件定义的 directives 中的 model, show 以及 components 中的 Transition, TransitionGroup 以外, components 中的KeepAlive 组件还有filters, _base属性我们没有看到。

我们再回到 core/index 文件入口(第一篇文章中, 找到 instance.js 文件也是从这里进入的), 看到:

import Vue from './instance/index' // 第一篇文章的入口
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue)
...

进入到 global-api/index.js, 搜索 Vue.options:

  import { ASSET_TYPES } from 'shared/constants'
  import builtInComponents from '../components/index'
  ...

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

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

// ../components/index
import KeepAlive from './keep-alive'
export default {
  KeepAlive
}

因此, 我们就找到了filters,_basecomponents 中的 KeepAlive。Vue 构造函数的 options 对象就很清晰了。

下面, 总结一下寻找基础 Vue 构造函数 options 选项的路径:

  • 1、new Vue({})实例化
  • 2、打印出vm.constructor.options, 发现有components, directives, filters, _base四个属性
  • 3、既然是项目初始化, 找到 package,json 文件, 找到项目启动命令: npm run dev
  • 4、根据 dev 命令行定位的 scripts/config 文件, 找到了入口文件 web/entry-runtime-with-compiler.js
  • 5、通过该入口文件中的import Vue from './runtime/index', 我们首先找到了 components 中的TransitionTransitionGroup组件;找到了 directives 属性中的modelshow指令
  • 6、再通过该文件import Vue from './runtime/index', 发现import Vue from 'core/index'
  • 7、回到入口文件core/index, 找到initGlobalApi, 进入global-api/index.js文件
  • 8、搜索 Vue.options, 找到了剩下的 components 中的keepAlive 组件以及filters_base属性 经过上面寻找路径, 我们最终找全了 Vue 构造器的最初的 options 选项:components, directives, filters, _base vm.constructor.options

通过基础构造器 new Vue()创建实例

Vue.extend 构造子类

再看 Ctor.super, 即通过 Vue.extend 构造子类
// Vue.extend 函数在实例上加了一个 super 属性
Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub['super'] = Super
  ...
}

/**
 * @param {*} Ctor: Ctor.super
 * 这个方法要分成两种情况来说明
 * 第一种是 Ctor 是基础 Vue 构造器的情况
 * 另一种是 Ctor 是通过 Vue.extend 方法扩展的情况。
 */
export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options // 相当于 vm.constructor.options
  // 有 super 属性,说明 Ctor 是 Vue.extend 构建的子类
  if (Ctor.super) { // 相当于 vm.constructor.super
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions // Vue 构造函数上的 options
    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
}

我们看下通过 Vue.extend()创建实例,举个例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>new Vue</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app2"></div>
  <script>
    var Profile = Vue.extend({
      template: '<p>123</p>',
    })
    console.log('new profile super ', new Profile().constructor.super)
    console.log('new profile superOptions', new Profile().constructor.superOptions)
    console.log('new profile constructor ', new Profile().constructor)
    console.log('new profile constructor options', new Profile().constructor.options)
    new Profile().$mount('#app2')
  </script>
</body>
</html>

我们找到 Vue.extend 函数的定义:

export function initExtend (Vue: GlobalAPI) {
 ...

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    ...
    const Sub = function VueComponent (options) {
      this._init(options) // 这里执行了 Vue 构造器上的_init 函数
    }
    Sub.prototype = Object.create(Super.prototype) // 继承 Super, 也就是 Vue
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
    ...

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

可以看到,在 Vue.extend 函数中, Sub(VueComponent)继承了 Super(Vue), 然后在 Sub 构造器上添加了

  • options: 融合了 Super.options 和 extendOptions(我们在 Vue.extend()中传入的选项)
  • super 属性: 指向 Super(Vue)
  • superOptions 属性: Super.options
  • extendOptions 属性: 传入的参数
  • sealedOptions 属性:Sub.options(见第一个)

这个时候, 我们打印一下super, superOptions, extendOptions, vm.constructor, vm.constructor.options 通过 Vue.extend 创建实例

从通过 Vue.extend 创建实例的图中, 可以看到

  • vm.constructor.super的值其实就是 Vue 构造器。而vm.constructor.superOptions其实是继承自基础 Vue 构造器上的选项。
  • extendOptions是我们传入的参数。
  • vm.constructor 是子类的构造器VueComponent,
  • vm.constructor.options的值,除了继承的四个基础属性: components, directives, filters, _base以外, 还有我们自定义的template属性。
  • superOptions + extendOptions = vm.constructor.options(vm.$options)

所以:

if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions // Vue 构造函数上的 options
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options. 将自身的 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
      }
    }
  }

这段代码的意思就是:将传入的选项以及父级 Vue 构造器上的选项进行合并返回 options。

当然,中间还有一个检查选项是否发生改变的函数 resolveModifiedOptions, 就不深入分析了。

小结

所以,resolveConstructorOptions函数的作用是:解析当前实例构造函数上的 options

在 Vue.extend 方法中涉及到 ES5 原型继承的知识, 如果不熟悉的话线下可以翻阅一下书籍。

到此我们就详细介绍了 vue 解析当前实例构造器上的 options 选项的原理, 下一篇我们继续介绍外层合并选项函数 mergeOptions 函数。

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

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