vue源码(三)Vue 构造函数
本文是学习vue源码,之所以转载过来是方便自己随时查看,在这里要感谢HcySunYang大神,提供的开源vue源码解析,写的非常非常好,简单易懂,比自己看要容易多了,他的文章链接地址是http://hcysun.me/vue-design/art/我们知道,我们在使用 Vue 的时候,要使用 new 操作符进行调用,这说明 Vue 应该是一个构造函数,所以我们要做的第一件事就是:把 Vue 构..
本文是学习vue源码,之所以转载过来是方便自己随时查看,在这里要感谢HcySunYang大神,提供的开源vue源码解析,写的非常非常好,简单易懂,比自己看要容易多了,他的文章链接地址是http://hcysun.me/vue-design/art/
我们知道,我们在使用 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
这五个文件中导入五个方法,分别是: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
为参数调用了 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
文件,这个文件将 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'
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
。这句话被写在了 rollup
的 replace
插件中,也就是说,__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
属性,这是一个对象,这个对象拥有四个属性分别是: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
文件,可以在 附录/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.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
。
#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
目录,可以发现有两个子目录 web
和 weex
。这两个子目录的作用就是分别为相应的平台对核心的 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
长什么样呢?要想知道这个问题,我们就要知道 platformDirectives
和 platformComponents
长什么样。
根据文件开头的 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
),分别是model
和show
。 - 在
Vue.options
上混合了两个组件(components
),分别是Transition
和TransitionGroup
。 - 在
Vue.prototype
上添加了两个方法:__patch__
和$mount
。
在经过这个文件之后,Vue.options
以及 Vue.config
和 Vue.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
。
之后缓存了运行时版 Vue
的 Vue.prototype.$mount
方法,并且进行了重写。
接下来又定义了 getOuterHTML
函数,用来获取一个元素的 outerHTML
。
这个文件运行下来,对 Vue
的影响有两个,第一个影响是它重写了 Vue.prototype.$mount
方法;第二个影响是添加了 Vue.compile
全局API,目前我们只需要获取这些信息就足够了,我们把这些影响同样更新到 附录
对应的文件中,也都可以查看的到。
到这里,Vue
神秘面具下真实的样子基本已经展现出来了。现在深呼吸,继续我们的探索吧!
更多推荐
所有评论(0)