作者:HcySunYang
请先看前文:Vue2.1.7——源码学习(一)
三、Vue 的构造函数是什么样的balabala一大堆,开始来干货吧。我们要做的第一件事就是搞清楚 Vue 构造函数到底是什么样子的。
我们知道,我们要使用 new 操作符来调用 Vue,那么也就是说 Vue 应该是一个构造函数,所以我们第一件要做的事儿就是把构造函数先扒的一清二楚,如何寻找 Vue 构造函数呢?当然是从 entry 开始啦,还记的我们运行 npm run dev 命令后,会输出 dist/vue.js 吗,那么我们就去看看 npm run dev 干了什么:
"dev": "TARGET=web-full-dev rollup -w -c build/config.js",
首先将 TARGET 得值设置为 ‘web-full-dev’,然后,然后,然后如果你不了解 rollup 就应该简单去看一下啦……,简单的说就是一个JavaScript模块打包器,你可以把它简单的理解为和 webpack 一样,只不过它有他的优势,比如 Tree-shaking (webpack2也有),但同样,在某些场景它也有他的劣势。。。废话不多说,其中 -w 就是watch,-c 就是指定配置文件为 build/config.js ,我们打开这个配置文件看一看:
// 引入依赖,定义 banner
...
// builds 对象
const builds = {
...
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
dest: path.resolve(__dirname, '../dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
}
// 生成配置的方法
function genConfig(opts){
...
}
if (process.env.TARGET) {
module.exports = genConfig(builds[process.env.TARGET])
} else {
exports.getBuild = name => genConfig(builds[name])
exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name]))
}
上面的代码是简化过的,当我们运行 npm run dev 的时候 process.env.TARGET 的值等于 ‘web-full-dev’,所以
module.exports = genConfig(builds[process.env.TARGET])
这句代码相当于:
module.exports = genConfig({
entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
dest: path.resolve(__dirname, '../dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
})
最终,genConfig 函数返回一个 config 对象,这个config对象就是Rollup的配置对象。那么我们就不难看到,入口文件是:
src/entries/web-runtime-with-compiler.js
我们打开这个文件,不要忘了我们的主题,我们在寻找Vue构造函数,所以当我们看到这个文件的第一行代码是:
import Vue from './web-runtime'
这个时候,你就应该知道,这个文件暂时与你无缘,你应该打开 web-runtime.js 文件,不过当你打开这个文件时,你发现第一行是这样的:
import Vue from 'core/index'
依照此思路,最终我们寻找到Vue构造函数的位置应该是在 src/core/instance/index.js 文件中,其实我们猜也猜得到,上面介绍目录的时候说过:instance 是存放Vue构造函数设计相关代码的目录。总结一下,我们寻找的过程是这样的:
寻找 Vue 构造函数路线
我们回头看一看 src/core/instance/index.js 文件,很简单:
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'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
引入依赖,定义 Vue 构造函数,然后以Vue构造函数为参数,调用了五个方法,最后导出 Vue。这五个方法分别来自五个文件:init.js state.js render.js events.js 以及 lifecycle.js。
打开这五个文件,找到相应的方法,你会发现,这些方法的作用,就是在 Vue 的原型 prototype 上挂载方法或属性,经历了这五个方法后的Vue会变成这样:
// initMixin(Vue) src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}
// stateMixin(Vue) src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
// renderMixin(Vue) src/core/instance/render.js **************************************************
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
Vue.prototype._s = _toString
Vue.prototype._v = createTextVNode
Vue.prototype._n = toNumber
Vue.prototype._e = createEmptyVNode
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = function(){}
Vue.prototype._o = function(){}
Vue.prototype._f = function resolveFilter (id) {}
Vue.prototype._l = function(){}
Vue.prototype._t = function(){}
Vue.prototype._b = function(){}
Vue.prototype._k = function(){}
// eventsMixin(Vue) src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
// lifecycleMixin(Vue) src/core/instance/lifecycle.js **************************************************
Vue.prototype._mount = function(){}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype._updateFromParent = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
这样就结束了吗?并没有,根据我们之前寻找 Vue 的路线,这只是刚刚开始,我们追溯路线往回走,那么下一个处理 Vue 构造函数的应该是 src/core/index.js 文件,我们打开它:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Vue.version = '__VERSION__'
export default Vue
这个文件也很简单,从 instance/index 中导入已经在原型上挂载了方法和属性后的 Vue,然后导入 initGlobalAPI 和 isServerRendering,之后将Vue作为参数传给 initGlobalAPI ,最后又在 Vue.prototype 上挂载了 $isServer ,在 Vue 上挂载了 version 属性。
initGlobalAPI 的作用是在 Vue 构造函数上挂载静态属性和方法,Vue 在经过 initGlobalAPI 之后,会变成这样:
// src/core/index.js / src/core/global-api/index.js
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
Vue.prototype.$isServer
Vue.version = '__VERSION__'
其中,稍微复杂一点的就是 Vue.options,大家稍微分析分析就会知道他的确长成那个样子。下一个就是 web-runtime.js 文件了,web-runtime.js 文件主要做了三件事儿:
1、覆盖 Vue.config 的属性,将其设置为平台特有的一些方法
2、Vue.options.directives 和 Vue.options.components 安装平台特有的指令和组件
3、在 Vue.prototype 上定义 patch 和 $mount
经过 web-runtime.js 文件之后,Vue 变成下面这个样子:
// 安装平台特定的utils
Vue.config.isUnknownElement = isUnknownElement
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.mustUseProp = mustUseProp
// 安装平台特定的 指令 和 组件
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount
这里大家要注意的是 Vue.options 的变化。另外这里的 $mount 方法很简单:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return this._mount(el, hydrating)
}
首先根据是否是浏览器环境决定要不要 query(el) 获取元素,然后将 el 作为参数传递给 this._mount()。
最后一个处理 Vue 的文件就是入口文件 web-runtime-with-compiler.js 了,该文件做了两件事:
1、缓存来自 web-runtime.js 文件的 $mount 函数
const mount = Vue.prototype.$mount
然后覆盖覆盖了 Vue.prototype.$mount
2、在 Vue 上挂载 compile
Vue.compile = compileToFunctions
compileToFunctions 函数的作用,就是将模板 template 编译为render函数。
至此,我们算是还原了 Vue 构造函数,总结一下:
四、一个贯穿始终的例子1、Vue.prototype 下的属性和方法的挂载主要是在 src/core/instance 目录中的代码处理的
2、Vue 下的静态属性和方法的挂载主要是在 src/core/global-api 目录下的代码处理的
3、web-runtime.js 主要是添加web平台特有的配置、组件和指令,web-runtime-with-compiler.js
给Vue的 $mount 方法添加 compiler 编译器,支持 template。
在了解了 Vue 构造函数的设计之后,接下来,我们一个贯穿始终的例子就要登场了,掌声有请:
let v = new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
好吧,我承认这段代码你家没满月的孩子都会写了。这段代码就是我们贯穿始终的例子,它就是这篇文章的主线,在后续的讲解中,都会以这段代码为例,当讲到必要的地方,会为其添加选项,比如讲计算属性的时候当然要加上一个 computed 属性了。不过在最开始,我只传递了两个选项 el 以及 data,“我们看看接下来会发生什么,让我们拭目以待“ —- NBA球星在接受采访时最喜欢说这句话。
当我们按照例子那样编码使用Vue的时候,Vue都做了什么?
想要知道Vue都干了什么,我们就要找到 Vue 初始化程序,查看 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)
}
我们发现,_init() 方法就是Vue调用的第一个方法,然后将我们的参数 options 透传了过去。在调用 _init() 之前,还做了一个安全模式的处理,告诉开发者必须使用 new 操作符调用 Vue。根据之前我们的整理,_init() 方法应该是在 src/core/instance/init.js 文件中定义的,我们打开这个文件查看 _init() 方法:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)
}
_init() 方法在一开始的时候,在 this 对象上定义了两个属性:_uid 和 _isVue,然后判断有没有定义 options._isComponent,在使用 Vue 开发项目的时候,我们是不会使用 _isComponent 选项的,这个选项是 Vue 内部使用的,按照本节开头的例子,这里会走 else 分支,也就是这段代码:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这样 Vue 第一步所做的事情就来了:使用策略对象合并参数选项
可以发现,Vue使用 mergeOptions 来处理我们调用Vue时传入的参数选项(options),然后将返回值赋值给 this.$options (vm === this),传给 mergeOptions 方法三个参数,我们分别来看一看,首先是:resolveConstructorOptions(vm.constructor),我们查看一下
这个方法:
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = Ctor.super.options
const cachedSuperOptions = Ctor.superOptions
const extendOptions = Ctor.extendOptions
if (superOptions !== cachedSuperOptions) {
// super option changed
Ctor.superOptions = superOptions
extendOptions.render = options.render
extendOptions.staticRenderFns = options.staticRenderFns
extendOptions._scopeId = options._scopeId
options = Ctor.options = mergeOptions(superOptions, extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
这个方法接收一个参数 Ctor,通过传入的 vm.constructor 我们可以知道,其实就是 Vue 构造函数本身。所以下面这句代码:
let options = Ctor.options
相当于:
let options = Vue.options
大家还记得 Vue.options 吗?在寻找Vue构造函数一节里,我们整理了 Vue.options 应该长成下面这个样子:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
之后判断是否定义了 Vue.super ,这个是用来处理继承的,我们后续再讲,在本例中,resolveConstructorOptions 方法直接返回了 Vue.options。也就是说,传递给 mergeOptions 方法的第一个参数就是 Vue.options。
传给 mergeOptions 方法的第二个参数是我们调用Vue构造函数时的参数选项,第三个参数是 vm 也就是 this 对象,按照本节开头的例子那样使用 Vue,最终运行的代码应该如下:
vm.$options = mergeOptions(
// Vue.options
{
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
},
// 调用Vue构造函数时传入的参数选项 options
{
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
},
// this
vm
)
了解了这些,我们就可以看看 mergeOptions 到底做了些什么了,根据引用寻找到 mergeOptions 应该是在 src/core/util/options.js 文件中定义的。这个文件第一次看可能会头大,下面是我处理后的简略展示,大家看上去应该更容易理解了:
// 1、引用依赖
import Vue from '../instance/index'
其他引用...
// 2、合并父子选项值为最终值的策略对象,此时 strats 是一个空对象,因为 config.optionMergeStrategies = Object.create(null)
const strats = config.optionMergeStrategies
// 3、在 strats 对象上定义与参数选项名称相同的方法
strats.el =
strats.propsData = function (parent, child, vm, key){}
strats.data = function (parentVal, childVal, vm)
config._lifecycleHooks.forEach(hook => {
strats[hook] = mergeHook
})
config._assetTypes.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
strats.watch = function (parentVal, childVal)
strats.props =
strats.methods =
strats.computed = function (parentVal: ?Object, childVal: ?Object)
// 默认的合并策略,如果有 `childVal` 则返回 `childVal` 没有则返回 `parentVal`
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
// 4、mergeOptions 中根据参数选项调用同名的策略方法进行合并处理
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 其他代码
...
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
上面的代码中,我省略了一些工具函数,例如 mergeHook 和 mergeAssets 等等,唯一需要注意的是这段代码:
config._lifecycleHooks.forEach(hook => {
strats[hook] = mergeHook
})
config._assetTypes.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
config 对象引用自 src/core/config.js 文件,最终的结果就是在 strats 下添加了相应的生命周期选项的合并策略函数为 mergeHook,添加指令(directives)、组件(components)、过滤器(filters)等选项的合并策略函数为 mergeAssets。
这样看来就清晰多了,拿我们贯穿本文的例子来说:
let v = new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
其中 el 选项会使用 defaultStrat 默认策略函数处理,data 选项则会使用 strats.data 策略函数处理,并且根据 strats.data 中的逻辑,strats.data 方法最终会返回一个函数:mergedInstanceDataFn。
这里就不详细的讲解每一个策略函数的内容了,后续都会讲到,这里我们还是抓住主线理清思路为主,只需要知道Vue在处理选项的时候,使用了一个策略对象对父子选项进行合并。并将最终的值赋值给实例下的 $options 属性即:this.$options,那么我们继续查看 _init() 方法在合并完选项之后,又做了什么:
合并完选项之后,Vue 第二部做的事情就来了:初始化工作与Vue实例对象的设计
前面讲了 Vue 构造函数的设计,并且整理了 Vue原型属性与方法 和 Vue静态属性与方法,而 Vue 实例对象就是通过构造函数创造出来的,让我们来看一看 Vue 实例对象是如何设计的,下面的代码是 _init() 方法合并完选项之后的代码:
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)
根据上面的代码,在生产环境下会为实例添加两个属性,并且属性值都为实例本身:
vm._renderProxy = vm
vm._self = vm
然后,调用了四个 init* 方法分别为:initLifecycle、initEvents、initState、initRender,且在 initState 前后分别回调了生命周期钩子 beforeCreate 和 created,而 initRender 是在 created 钩子执行之后执行的,看到这里,也就明白了为什么 created 的时候不能操作DOM了。因为这个时候还没有渲染真正的DOM元素到文档中。created 仅仅代表数据状态的初始化完成。
根据四个 init* 方法的引用关系打开对应的文件查看对应的方法,我们发现,这些方法是在处理Vue实例对象,以及做一些初始化的工作,类似整理Vue构造函数一样,我同样针对Vue实例做了属性和方法的整理,如下:
// 在 Vue.prototype._init 中添加的属性 **********************************************************
this._uid = uid++
this._isVue = true
this.$options = {
components,
directives,
filters,
_base,
el,
data: mergedInstanceDataFn()
}
this._renderProxy = this
this._self = this
// 在 initLifecycle 中添加的属性 **********************************************************
this.$parent = parent
this.$root = parent ? parent.$root : this
this.$children = []
this.$refs = {}
this._watcher = null
this._inactive = false
this._isMounted = false
this._isDestroyed = false
this._isBeingDestroyed = false
// 在 initEvents 中添加的属性 **********************************************************
this._events = {}
this._updateListeners = function(){}
// 在 initState 中添加的属性 **********************************************************
this._watchers = []
// initData
this._data
// 在 initRender 中添加的属性 **********************************************************
this.$vnode = null // the placeholder node in parent tree
this._vnode = null // the root of the child tree
this._staticTrees = null
this.$slots
this.$scopedSlots
this._c
this.$createElement
以上就是一个Vue实例所包含的属性和方法,除此之外要注意的是,在 initEvents 中除了添加属性之外,如果有 vm.$options._parentListeners 还要调用 vm._updateListeners() 方法,在 initState 中又调用了一些其他init方法,如下:
export function initState (vm: Component) {
vm._watchers = []
initProps(vm)
initMethods(vm)
initData(vm)
initComputed(vm)
initWatch(vm)
}
最后在 initRender 中如果有 vm.$options.el 还要调用 vm.$mount(vm.$options.el),如下:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
这就是为什么如果不传递 el 选项就需要手动 mount 的原因了。
那么我们依照我们本节开头的的例子,以及初始化的先后顺序来逐一看一看都发生了什么。我们将 initState 中的 init* 方法展开来看,执行顺序应该是这样的(从上到下的顺序执行):
initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initProps(vm)
initMethods(vm)
initData(vm)
initComputed(vm)
initWatch(vm)
callHook(vm, 'created')
initRender(vm)
首先是 initLifecycle,这个函数的作用就是在实例上添加一些属性,然后是 initEvents,由于 vm.$options._parentListeners 的值为 undefined 所以也仅仅是在实例上添加属性, vm._updateListeners(listeners) 并不会执行,由于我们只传递了 el 和 data,所以 initProps、initMethods、initComputed、initWatch 这四个方法什么都不会做,只有 initData 会执行。最后是 initRender,除了在实例上添加一些属性外,由于我们传递了 el 选项,所以会执行 vm.$mount(vm.$options.el)。
综上所述:按照我们的例子那样写,初始化工作只包含两个主要内容即:initData 和 initRender。
未完紧接:Vue2.1.7——源码学习(三)