Vue 构造函数

我们知道,我们在使用 Vue 的时候,要使用 new 操作符进行调用,这说明 Vue 应该是一个构造函数,所以我们要做的第一件事就是:把 Vue 构造函数搞清楚。

Vue 构造函数的原型

了解 Vue 这个项目 一节中,我们在最后提到这套文章将会以 npm run dev 为切入点:

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",

当我们执行 npm run dev 时,根据 scripts/config.js 文件中的配置:

  // 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,最终输出 dist/vue.js,它是一个 umd 模块,接下来我们就以入口文件为起点,找到 Vue 构造函数并将 Vue 构造函数的真面目扒的一清二楚。

但现在有一个问题 web/entry-runtime-with-compiler.js 中这个 web 指的是哪一个目录?这其实是一个别名配置,打开 scripts/alias.js 文件:

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  entries: resolve('src/entries'),
  sfc: resolve('src/sfc')
}

其中有这么一句:

web: resolve('src/platforms/web')

所以 web 指向的应该是 src/platforms/web,除了 web 之外,alias.js 文件中还配置了其他的别名,大家在找对应目录的时候,可以来这里查阅,后面就不做这种目录寻找的说明了。

接下来我们就进入正题,打开 src/platforms/web/entry-runtime-with-compiler.js 文件,你可以看到这样一句话:

import Vue from './runtime/index'

这说明:这个文件并不是 Vue 构造函数的“出生地”,这个文件中的 Vue 是从 ./runtime/index 导入进来的,于是我们就打开当前目录的 runtime 目录下的 index.js 看一下,你同样能够发现这样一句话:

import Vue from 'core/index'

同样的道理,这说明 runtime/index.js 文件也不是 Vue 构造函数的“出生地”,你应该继续顺藤摸瓜打开 core/index.js 文件,在 scripts/alias.js 的配置中,core 指向的是 src/core,打开 src/core/index.js 你能看到这样一句:

import Vue from './instance/index'

按照之前的套路,继续打开 ./instance/index.js 文件:

// 从五个文件导入五个方法(不包括 warn)
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

// 定义 Vue 构造函数
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

// 将 Vue 作为参数传递给导入的五个方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

// 导出 Vue
export default Vue

可以看到,这个文件才是 Vue 构造函数真正的“出生地”,上面的代码是 ./instance/index.js 文件中全部的代码,还是比较简短易看的,首先分别从 ./init.js./state.js./render.js./events.js./lifecycle.js 这五个文件中导入五个方法,分别是:initMixinstateMixinrenderMixineventsMixin 以及 lifecycleMixin,然后定义了 Vue 构造函数,其中使用了安全模式来提醒你要使用 new 操作符来调用 Vue,接着将 Vue 构造函数作为参数,分别传递给了导入进来的这五个方法,最后导出 Vue

那么这五个方法又做了什么呢?先看看 initMixin ,打开 ./init.js 文件,找到 initMixin 方法,如下:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ... _init 方法的函数体,此处省略
  }
}

这个方法的作用就是在 Vue 的原型上添加了 _init 方法,这个 _init 方法看上去应该是内部初始化的一个方法,其实在 instance/index.js 文件中我们是见过这个方法的,如下:

// 定义 Vue 构造函数
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 在这里
  this._init(options)
}

Vue 的构造函数里有这么一句:this._init(options),这说明,当我们执行 new Vue() 的时候,this._init(options) 将被执行。

再打开 ./state.js 文件,找到 stateMixin 方法,这个方法的一开始,是这样一段代码:

  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function (newData: Object) {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

我们先看最后两句,使用 Object.definePropertyVue.prototype 上定义了两个属性,就是大家熟悉的:$data$props,这两个属性的定义分别写在了 dataDef 以及 propsDef 这两个对象里,我们来仔细看一下这两个对象的定义,首先是 get

const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }

可以看到,$data 属性实际上代理的是 _data 这个实例属性,而 $props 代理的是 _props 这个实例属性。然后有一个是否为生产环境的判断,如果不是生产环境的话,就为 $data$props 这两个属性设置一下 set,实际上就是提示你一下:别他娘的想修改我,老子无敌。

也就是说,$data$props 是两个只读的属性,所以,现在让你使用 js 实现一个只读的属性,你应该知道要怎么做了。

接下来 stateMixin 又在 Vue.prototype 上定义了三个方法:

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
  	// ...
  }

这三个方法分别是:$set$delete 以及 $watch,实际上这些东西你都见过,在这里:

然后是 eventsMixin 方法,这个方法在 ./events.js 文件中,打开这个文件找到 eventsMixin 方法,这个方法在 Vue.prototype 上添加了四个方法,分别是:

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}

下一个是 lifecycleMixin,打开 ./lifecycle.js 文件找到相应方法,这个方法在 Vue.prototype 上添加了三个方法:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}

最后一个就是 renderMixin 方法了,它在 render.js 文件中,这个方法的一开始以 Vue.prototype 为参数调用了 installRenderHelpers 函数,这个函数来自于与 render.js 文件相同目录下的 render-helpers/index.js 文件,打开这个文件找到 installRenderHelpers 函数:

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

以上代码就是 installRenderHelpers 函数的源码,可以发现,这个函数的作用就是在 Vue.prototype 上添加一系列方法,这些方法的作用大家暂时还不需要关心,后面都会讲解到。

renderMixin 方法在执行完 installRenderHelpers 函数之后,又在 Vue.prototype 上添加了两个方法,分别是:$nextTick_render,最终经过 renderMixin 之后,Vue.prototype 又被添加了如下方法:

// installRenderHelpers 函数中
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners

Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}

至此,instance/index.js 文件中的代码就运行完毕了(注意:所谓的运行,是指执行 npm run dev 命令时构建的运行)。我们大概了解了每个 *Mixin 方法的作用其实就是包装 Vue.prototype,在其上挂载一些属性和方法,下面我们要做一件很重要的事情,就是将上面的内容集中合并起来,放到一个单独的地方,便于以后查看,我将它们整理到了这里:附录/Vue 构造函数整理-原型,这样当我们在后面详细讲解的时候,提到某个方法你就可以迅速定位它的位置,以便于保持我们思路的清晰。

Vue 构造函数的静态属性和方法(全局API)

到目前为止,core/instance/index.js 文件,也就是 Vue 的出生文件的代码我们就看完了,按照之前我们寻找 Vue 构造函数时的文件路径回溯,下一个我们要看的文件应该就是 core/index.js 文件,这个文件将 Vuecore/instance/index.js 文件中导入了进来,我们打开 core/index.js 文件,下面是其全部的代码,同样很简短易看:

// 从 Vue 的出生文件导入 Vue
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

// 将 Vue 构造函数作为参数,传递给 initGlobalAPI 方法,该方法来自 ./global-api/index.js 文件
initGlobalAPI(Vue)

// 在 Vue.prototype 上添加 $isServer 属性,该属性代理了来自 core/util/env.js 文件的 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

// 在 Vue.prototype 上添加 $ssrContext 属性
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

// Vue.version 存储了当前 Vue 的版本号
Vue.version = '__VERSION__'

// 导出 Vue
export default Vue

上面的代码中,首先从 Vue 的出生文件,也就是 instance/index.js 文件导入 Vue,然后分别从三个文件导入了三个变量,如下:

import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

其中 initGlobalAPI 是一个函数,并且以 Vue 构造函数作为参数进行调用:

initGlobalAPI(Vue)

然后在 Vue.prototype 上分别添加了两个只读的属性,分别是:$isServer$ssrContext。接着又在 Vue 构造函数上定义了 FunctionalRenderContext 静态属性,并且 FunctionalRenderContext 属性的值为来自 core/vdom/create-functional-component.js 文件的 FunctionalRenderContext,之所以在 Vue 构造函数上暴露该属性,是为了在 ssr 中使用它。

最后,在 Vue 构造函数上添加了一个静态属性 version,存储了当前 Vue 的版本值,但是这里的 '__VERSION__' 是什么鬼?打开 scripts/config.js 文件,找到 genConfig 方法,其中有这么一句话:__VERSION__: version。这句话被写在了 rollupreplace 插件中,也就是说,__VERSION__ 最终将被 version 的值替换,而 version 的值就是 Vue 的版本号。

我们再回过头来看看这句代码:

initGlobalAPI(Vue)

大家应该可以猜个大概,这看上去像是在 Vue 上添加一些全局的API,实际上就是这样的,这些全局API以静态属性和方法的形式被添加到 Vue 构造函数上,打开 src/core/global-api/index.js 文件找到 initGlobalAPI 方法,我们来看看 initGlobalAPI 方法都做了什么。

首先是这样一段代码:

  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

这段代码的作用是在 Vue 构造函数上添加 config 属性,这个属性的添加方式类似我们前面看过的 $data 以及 $props,也是一个只读的属性,并且当你试图设置其值时,在非生产环境下会给你一个友好的提示。

Vue.config 的值是什么呢?在 src/core/global-api/index.js 文件的开头有这样一句:

import config from '../config'

所以 Vue.config 代理的是从 core/config.js 文件导出的对象。

接着是这样一段代码:

// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
	warn,
	extend,
	mergeOptions,
	defineReactive
}

Vue 上添加了 util 属性,这是一个对象,这个对象拥有四个属性分别是:warnextendmergeOptions 以及 defineReactive。这四个属性来自于 core/util/index.js 文件。

这里有一段注释,大概意思是 Vue.util 以及 util 下的四个方法都不被认为是公共API的一部分,要避免依赖他们,但是你依然可以使用,只不过风险你要自己控制。并且,在官方文档上也并没有介绍这个全局API,所以能不用尽量不要用。

然后是这样一段代码:

Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick

Vue.options = Object.create(null)

这段代码比较简单,在 Vue 上添加了四个属性分别是 setdeletenextTick 以及 options,这里要注意的是 Vue.options,现在它还只是一个空的对象,通过 Object.create(null) 创建。

不过接下来,Vue.options 就不是一个空的对象了,因为下面这段代码:

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)

上面的代码中,ASSET_TYPES 来自于 shared/constants.js 文件,打开这个文件,发现 ASSET_TYPES 是一个数组:

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

所以当下面这段代码执行完后:

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

Vue.options 将变成这样:

Vue.options = {
	components: Object.create(null),
	directives: Object.create(null),
	filters: Object.create(null),
	_base: Vue
}

紧接着,是这句代码:

extend(Vue.options.components, builtInComponents)

extend 来自于 shared/util.js 文件,可以在 附录/shared/util.js 文件工具方法全解 中查看其作用,总之这句代码的意思就是将 builtInComponents 的属性混合到 Vue.options.components 中,其中 builtInComponents 来自于 core/components/index.js 文件,该文件如下:

import KeepAlive from './keep-alive'

export default {
  KeepAlive
}

所以最终 Vue.options.components 的值如下:

Vue.options.components = {
	KeepAlive
}

那么到现在为止,Vue.options 已经变成了这样:

Vue.options = {
	components: {
		KeepAlive
	},
	directives: Object.create(null),
	filters: Object.create(null),
	_base: Vue
}

我们继续看代码,在 initGlobalAPI 方法的最后部分,以 Vue 为参数调用了四个 init* 方法:

initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)

这四个方法从上至下分别来自于 global-api/use.jsglobal-api/mixin.jsglobal-api/extend.js 以及 global-api/assets.js 这四个文件,我们不着急,一个一个慢慢地看,先打开 global-api/use.js 文件,我们发现这个文件只有一个 initUse 方法,如下:

/* @flow */

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // ...
  }
}

该方法的作用是在 Vue 构造函数上添加 use 方法,也就是传说中的 Vue.use 这个全局API,这个方法大家应该不会陌生,用来安装 Vue 插件。

再打开 global-api/mixin.js 文件,这个文件更简单,全部代码如下:

/* @flow */

import { mergeOptions } from '../util/index'

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

其中,initMixin 方法的作用是,在 Vue 上添加 mixin 这个全局API。

再打开 global-api/extend.js 文件,找到 initExtend 方法,如下:

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    // ...
  }
}

initExtend 方法在 Vue 上添加了 Vue.cid 静态属性,和 Vue.extend 静态方法。

最后一个是 initAssetRegisters,我们打开 global-api/assets.js 文件,找到 initAssetRegisters 方法如下:

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      // ......
    }
  })
}

其中,ASSET_TYPES 我们已经见过了,它在 shared/constants.js 文件中,长成这样:

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

所以,最终经过 initAssetRegisters 方法,Vue 又多了三个静态方法:

Vue.component
Vue.directive
Vue.filter

这三个静态方法大家都不陌生,分别用来全局注册组件,指令和过滤器。

这样,initGlobalAPI 方法的全部功能我们就介绍完毕了,它的作用就像它的名字一样,是在 Vue 构造函数上添加全局的API,类似整理 Vue.prototype 上的属性和方法一样,我们同样对 Vue 静态属性和方法做一个整理,将它放到 附录/Vue 构造函数整理-全局API 中,便于以后查阅。

至此,对于 core/index.js 文件的作用我们也大概清楚了,在这个文件里,它首先将核心的 Vue,也就是在 core/instance/index.js 文件中的 Vue,也可以说是原型被包装(添加属性和方法)后的 Vue 导入,然后使用 initGlobalAPI 方法给 Vue 添加静态方法和属性,除此之外,在这个文件里,也对原型进行了修改,为其添加了两个属性:$isServer$ssrContext,最后添加了 Vue.version 属性并导出了 Vue

Vue 平台化的包装

现在,在我们弄清 Vue 构造函数的过程中已经看了两个主要的文件,分别是:core/instance/index.js 文件以及 core/index.js 文件,前者是 Vue 构造函数的定义文件,我们一直都叫其 Vue 的出生文件,主要作用是定义 Vue 构造函数,并对其原型添加属性和方法,即实例属性和实例方法。后者的主要作用是,为 Vue 添加全局的API,也就是静态的方法和属性。这两个文件有个共同点,就是它们都在 core 目录下,我们在介绍 Vue 项目目录结构的时候说过:core 目录存放的是与平台无关的代码,所以无论是 core/instance/index.js 文件还是 core/index.js 文件,它们都在包装核心的 Vue,且这些包装是与平台无关的。但是,Vue 是一个 Multi-platform 的项目(web和weex),不同平台可能会内置不同的组件、指令,或者一些平台特有的功能等等,那么这就需要对 Vue 根据不同的平台进行平台化地包装,这就是接下来我们要看的文件,这个文件也出现在我们寻找 Vue 构造函数的路线上,它就是:platforms/web/runtime/index.js 文件。

在看这个文件之前,大家可以先打开 platforms 目录,可以发现有两个子目录 webweex。这两个子目录的作用就是分别为相应的平台对核心的 Vue 进行包装的。而我们所要研究的 web 平台,就在 web 这个目录里。

接下来,我们就打开 platforms/web/runtime/index.js 文件,看一看里面的代码,这个文件的一开始,是一大堆 import 语句,其中就包括从 core/index.js 文件导入 Vue 的那句。

import 语句下面是这样一段代码:

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

大家还记得 Vue.config 吗?其代理的值是从 core/config.js 文件导出的对象,这个对象最开始长成这样:

Vue.config = {
  optionMergeStrategies: Object.create(null),
  silent: false,
  productionTip: process.env.NODE_ENV !== 'production',
  devtools: process.env.NODE_ENV !== 'production',
  performance: false,
  errorHandler: null,
  warnHandler: null,
  ignoredElements: [],
  keyCodes: Object.create(null),
  isReservedTag: no,
  isReservedAttr: no,
  isUnknownElement: no,
  getTagNamespace: noop,
  parsePlatformTagName: identity,
  mustUseProp: no,
  _lifecycleHooks: LIFECYCLE_HOOKS
}

我们可以看到,从 core/config.js 文件导出的 config 对象,大部分属性都是初始化了一个初始值,并且我们在 core/config.js 文件中能看到很多这样的注释,如下图:

This is platform-dependent and may be overwritten.,这句话的意思是,这个配置是与平台有关的,很可能会被覆盖掉。这个时候我们再回来看这段代码:

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

其实这就是在覆盖默认导出的 config 对象的属性,注释已经写得很清楚了,安装平台特定的工具方法,至于这些东西的作用这里我们暂且不说,你只要知道它在干嘛即可。

接着是这两句代码:

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

安装特定平台运行时的指令和组件,大家还记得 Vue.options 长什么样吗?在执行这两句代码之前,它长成这样:

Vue.options = {
	components: {
		KeepAlive
	},
	directives: Object.create(null),
	filters: Object.create(null),
	_base: Vue
}

extend 方法我们见过,这里就不说明其作用了,可以查看 附录/shared/util.js 文件工具方法全解,那么经过这两句代码之后的 Vue.options 长什么样呢?要想知道这个问题,我们就要知道 platformDirectivesplatformComponents 长什么样。

根据文件开头的 import 语句:

import platformDirectives from './directives/index'
import platformComponents from './components/index'

我们知道,这两个变量来自于 runtime/directives/index.js 文件和 runtime/components/index.js 文件,我们先打开 runtime/directives/index.js 文件,下面是其全部代码:

import model from './model'
import show from './show'

export default {
  model,
  show
}

也就是说,platformDirectives 是:

platformDirectives = {
  model,
  show
}

所以,经过:

extend(Vue.options.directives, platformDirectives)

这句代码之后,Vue.options 将变为:

Vue.options = {
	components: {
		KeepAlive
	},
	directives: {
		model,
		show
	},
	filters: Object.create(null),
	_base: Vue
}

同样的道理,下面是 runtime/components/index.js 文件全部的代码:

import Transition from './transition'
import TransitionGroup from './transition-group'

export default {
  Transition,
  TransitionGroup
}

所以 platformComponents 的值为:

platformComponents = {
  Transition,
  TransitionGroup
}

那么经过:

extend(Vue.options.components, platformComponents)

之后,Vue.options 将变为:

Vue.options = {
	components: {
		KeepAlive,
		Transition,
		TransitionGroup
	},
	directives: {
		model,
		show
	},
	filters: Object.create(null),
	_base: Vue
}

这样,这两句代码的目的我们就搞清楚了,其作用是在 Vue.options 上添加 web 平台运行时的特定组件和指令。

我们继续往下看代码,接下来是这段:

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

首先在 Vue.prototype 上添加 __patch__ 方法,如果在浏览器环境运行的话,这个方法的值为 patch 函数,否则是一个空函数 noop。然后又在 Vue.prototype 上添加了 $mount 方法,我们暂且不关心 $mount 方法的内容和作用。

再往下的一段代码是 vue-devtools 的全局钩子,它被包裹在 setTimeout 中,最后导出了 Vue

现在我们就看完了 platforms/web/runtime/index.js 文件,该文件的作用是对 Vue 进行平台化地包装:

  • 设置平台化的 Vue.config
  • Vue.options 上混合了两个指令(directives),分别是 modelshow
  • Vue.options 上混合了两个组件(components),分别是 TransitionTransitionGroup
  • Vue.prototype 上添加了两个方法:__patch__$mount

在经过这个文件之后,Vue.options 以及 Vue.configVue.prototype 都有所变化,我们把这些变化更新到对应的 附录 文件里,都可以查看的到。

with compiler

在看完 runtime/index.js 文件之后,其实 运行时 版本的 Vue 构造函数就已经“成型了”。我们可以打开 entry-runtime.js 这个入口文件,这个文件只有两行代码:

import Vue from './runtime/index'

export default Vue

可以发现,运行时 版的入口文件,导出的 Vue 就到 ./runtime/index.js 文件为止。然而我们所选择的并不仅仅是运行时版,而是完整版的 Vue,入口文件是 entry-runtime-with-compiler.js,我们知道完整版和运行时版的区别就在于 compiler,所以其实在我们看这个文件的代码之前也能够知道这个文件的作用:就是在运行时版的基础上添加 compiler,对没错,这个文件就是干这个的,接下来我们就看看它是怎么做的,打开 entry-runtime-with-compiler.js 文件:

// ... 其他 import 语句

// 导入 运行时 的 Vue
import Vue from './runtime/index'

// ... 其他 import 语句

// 从 ./compiler/index.js 文件导入 compileToFunctions
import { compileToFunctions } from './compiler/index'

// 根据 id 获取元素的 innerHTML
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

// 使用 mount 变量缓存 Vue.prototype.$mount 方法
const mount = Vue.prototype.$mount
// 重写 Vue.prototype.$mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // ... 函数体省略
}

/**
 * 获取元素的 outerHTML
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

// 在 Vue 上添加一个全局API `Vue.compile` 其值为上面导入进来的 compileToFunctions
Vue.compile = compileToFunctions

// 导出 Vue
export default Vue

上面代码是简化过的,但是保留了所有重要的部分,该文件的开始是一堆 import 语句,其中重要的两句 import 语句就是上面代码中出现的那两句,一句是导入运行时的 Vue,一句是从 ./compiler/index.js 文件导入 compileToFunctions,并且在倒数第二句代码将其添加到 Vue.compile 上。

然后定义了一个函数 idToTemplate,这个函数的作用是:获取拥有指定 id 属性的元素的 innerHTML

之后缓存了运行时版 VueVue.prototype.$mount 方法,并且进行了重写。

接下来又定义了 getOuterHTML 函数,用来获取一个元素的 outerHTML

这个文件运行下来,对 Vue 的影响有两个,第一个影响是它重写了 Vue.prototype.$mount 方法;第二个影响是添加了 Vue.compile 全局API,目前我们只需要获取这些信息就足够了,我们把这些影响同样更新到 附录 对应的文件中,也都可以查看的到。

到这里,Vue 神秘面具下真实的样子基本已经展现出来了。现在深呼吸,继续我们的探索吧!