Vue构造函数.md 20 KB

Vue 构造函数

Vue 构造函数的原型

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

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

当我们执行 npm run dev 时,根据 build/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 指的是哪一个目录?这其实是一个别名配置,打开 build/alias.js 文件:

const path = require('path')

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

其中有这么一句:

web: path.resolve(__dirname, '../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 文件,在 build/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 添加了一大堆的方法,我们暂且不管是干什么,先列出来,如下:

Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
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

至此,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'

// 将 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
  }
})

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

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

initGlobalAPI(Vue)

然后在 Vue.prototype 上分别添加了两个只读的属性,分别是:$isServer$ssrContext

最后,在 Vue 构造函数上添加了一个静态属性 version,存储了当前 Vue 的版本值,但是这里的 '__VERSION__' 是什么鬼?打开 build/config.js 文件,找到 genConfig 方法,如下:

也就是说,__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,也是一个只读的属性,并且当你试图设置其值时,在非生产环境下会给你一个友好的提示,为什么说它友好呢?因为如果是我的话,我可能会提示你:what are you fucking doing

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 文件,它长成这样:

/**
 * 将 _from 对象的属性混合到 to 对象中
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

这是一个很简单的方法,用来混合两个对象的属性,所以下面这段代码:

extend(Vue.options.components, builtInComponents)

的意思就是将 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