在 了解 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
这五个文件中导出五个方法,分别是:initMixin
、stateMixin
、renderMixin
、eventsMixin
以及 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.defineProperty
在 Vue.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 构造函数整理-原型,其中 对原型的包装一节
是对上面内容的整理,这样当我们在后面的详细讲解的时候,提到某个方法你就可以迅速定位它的位置,便于我们思路的清晰。
到目前为止,core/instance/index.js
文件,也就是 Vue
的出生文件的代码我们就看完了,按照之前我们寻找Vue构造函数时的文件路径回溯,下一个我们要看的文件应该就是 core/index.js
文件,这个文件将 Vue
从 core/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
属性,这是一个对象,这个对象拥有四个属性分别是:warn
、extend
、mergeOptions
以及 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
上添加了四个属性分别是 set
、delete
、nextTick
以及 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.js
、global-api/mixin.js
、global-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
。