合并配置
通过之前章节的源码分析我们知道,new Vue
的过程通常有 2 种场景,一种是外部我们的代码主动调用 new Vue(options)
的方式实例化一个 Vue 对象;另一种是我们上一节分析的组件过程中内部通过 new Vue(options)
实例化子组件。
无论哪种场景,都会执行实例的 _init(options)
方法,它首先会执行一个 merge options
的逻辑,相关的代码在 src/core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
// 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
)
}
// ...
}
可以看到不同场景对于 options
的合并逻辑是不一样的,并且传入的 options
值也有非常大的不同,接下来我会分开介绍 2 种场景的 options 合并过程。
为了更直观,我们可以举个简单的示例:
import Vue from 'vue'
let childComp = {
template: '<div>{{msg}}</div>',
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
data() {
return {
msg: 'Hello Vue'
}
}
}
Vue.mixin({
created() {
console.log('parent created')
}
})
let app = new Vue({
el: '#app',
render: h => h(childComp)
})
外部调用场景
当执行 new Vue
的时候,在执行 this._init(options)
的时候,就会执行如下逻辑去合并 options
:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这里通过调用 mergeOptions
方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor)
的返回值和 options
做合并,resolveConstructorOptions
的实现先不考虑,在我们这个场景下,它还是简单返回 vm.constructor.options
,相当于 Vue.options
,那么这个值又是什么呢,其实在 initGlobalAPI(Vue)
的时候定义了这个值,代码在 src/core/global-api/index.js
中:
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
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)
// ...
}
首先通过 Vue.options = Object.create(null)
创建一个空对象,然后遍历 ASSET_TYPES
,ASSET_TYPES
的定义在 src/shared/constants.js
中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
所以上面遍历 ASSET_TYPES
后的代码相当于:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
接着执行了 Vue.options._base = Vue
,它的作用在我们上节实例化子组件的时候介绍了。
最后通过 extend(Vue.options.components, builtInComponents)
把一些内置组件扩展到 Vue.options.components
上,Vue 的内置组件目前有 <keep-alive>
、<transition>
和 <transition-group>
组件,这也就是为什么我们在其它组件中使用 <keep-alive>
组件不需要注册的原因,这块儿后续我们介绍 <keep-alive>
组件的时候会详细讲。
那么回到 mergeOptions
这个函数,它的定义在 src/core/util/options.js
中:
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
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
}
mergeOptions
主要功能就是把 parent
和 child
这两个对象根据一些合并策略,合并成一个新对象并返回。比较核心的几步,先递归把 extends
和 mixixns
合并到 parent
上,然后遍历 parent
,调用 mergeField
,然后再遍历 child
,如果 key
不在 perent
的自身属性上,则调用 mergeField
。
这里有意思的是 mergeField
函数,它对不同的 key
有着不同的合并策略。举例来说,对于生命周期函数,它的合并策略是这样的:
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
这其中的 LIFECYCLE_HOOKS
的定义在 src/shared/constants.js
中:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
这里定义了 Vue.js 所有的钩子函数名称,所以对于钩子函数,他们的合并策略都是 mergeHook
函数。这个函数的实现也非常有意思,用了一个多层 3 元运算符,逻辑就是如果不存在 childVal
,就返回 parentVal
;否则再判断是否存在 parentVal
,如果存在就把 childVal
添加到 parentVal
后返回新数组;否则返回 childVal
的数组。所以回到 mergeOptions
函数,一旦 parent
和 child
都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
关于其它属性的合并策略的定义都可以在 src/core/util/options.js
文件中看到,这里不一一介绍了,感兴趣的同学可以自己看。
通过执行 mergeField
函数,把合并后的结果保存到 options
对象中,最终返回它。
因此,在我们当前这个 case 下,执行完如下合并后:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm.$options
的值差不多是如下这样:
vm.$options = {
components: { },
created: [
function created() {
console.log('parent created')
}
],
directives: { },
filters: { },
_base: function Vue(options) {
// ...
},
el: "#app",
render: function (h) {
//...
}
}
组件场景
由于组件的构造函数是通过 Vue.extend
继承自 Vue
的,先回顾一下这个过程,代码定义在 src/core/global-api/extend.js
中。
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// ...
return Sub
}
我们只保留关键逻辑,这里的 extendOptions
对应的就是前面定义的组件对象,它会和 Vue.options
合并到 Sub.opitons
中。
接下来我们再回忆一下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js
中:
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// ...
return new vnode.componentOptions.Ctor(options)
}
这里的 vnode.componentOptions.Ctor
就是指向 Vue.extend
的返回值 Sub
, 所以 执行 new vnode.componentOptions.Ctor(options)
接着执行 this._init(options)
,因为 options._isComponent
为 true,那么合并 options
的过程走到了 initInternalComponent(vm, options)
逻辑。先来看一下它的代码实现,在 src/core/instance/init.js
中:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
initInternalComponent
方法首先执行 const opts = vm.$options = Object.create(vm.constructor.options)
,这里的 vm.construction
就是子组件的构造函数 Sub
,相当于 vm.$options = Sub.options
。
接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode
、子组件的父 Vue 实例 parent
保存到 vm.$options
中,另外还保留了 parentVnode
配置中的如 propsData
等其它的属性。
这么看来,initInternalComponent
只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。
因此,在我们当前这个 case 下,执行完如下合并后:
initInternalComponent(vm, options)
vm.$options
的值差不多是如下这样:
vm.$options = {
parent: Vue /*父Vue实例*/,
propsData: undefined,
_componentTag: undefined,
_parentVnode: VNode /*父VNode实例*/,
_renderChildren:undefined,
__proto__: {
components: { },
directives: { },
filters: { },
_base: function Vue(options) {
//...
},
_Ctor: {},
created: [
function created() {
console.log('parent created')
}, function created() {
console.log('child created')
}
],
mounted: [
function mounted() {
console.log('child mounted')
}
],
data() {
return {
msg: 'Hello Vue'
}
},
template: '<div>{{msg}}</div>'
}
}
生命周期
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。
<img src="../assets/lifecycle.png"/>
在我们实际项目开发过程中,会非常频繁地和 Vue 组件的生命周期打交道,接下来我们就从源码的角度来看一下这些生命周期的钩子函数是如何被执行的。
源码中最终执行生命周期的函数都是调用 callHook
方法,它的定义在 src/core/instance/lifecycle
中:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
callHook
函数的逻辑很简单,根据传入的字符串 hook
,去拿到 vm.$options[hook]
对应的回调函数数组,然后遍历执行,执行的时候把 vm
作为函数执行的上下文。
在上一节中,我们详细地介绍了 Vue.js 合并 options
的过程,各个阶段的生命周期的函数也被合并到 vm.$options
里,并且是一个数组。因此 callhook
函数的功能就是调用某个生命周期钩子注册的所有回调函数。
了解了生命周期的执行方式后,接下来我们会具体介绍每一个生命周期函数它的调用时机。
beforeCreate & created
beforeCreate
和 created
函数都是在实例化 Vue
的阶段,在 _init
方法中执行的,它的定义在 src/core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
可以看到 beforeCreate
和 created
的钩子调用是在 initState
的前后,initState
的作用是初始化 props
、data
、methods
、watch
、computed
等属性,之后我们会详细分析。那么显然 beforeCreate
的钩子函数中就不能获取到 props
、data
中定义的值,也不能调用 methods
中定义的函数。
在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props
、data
等数据的话,就需要使用 created
钩子函数。之后我们会介绍 vue-router 和 vuex 的时候会发现它们都混合了 beforeCreatd
钩子函数。
beforeMount & mounted
顾名思义,beforeMount
钩子函数发生在 mount
,也就是 DOM 挂载之前,它的调用时机是在 mountComponent
函数中,定义在 src/core/instance/lifecycle.js
中:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
在执行 vm._render()
函数渲染 VNode 之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 VNode patch 到真实 DOM 后,执行 mouted
钩子。注意,这里对 mouted
钩子函数执行有一个判断逻辑,vm.$vnode
如果为 null
,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue
初始化过程。那么对于组件,它的 mounted
时机在哪儿呢?
之前我们提到过,组件的 VNode patch 到 DOM 后,会执行 invokeInsertHook
函数,把 insertedVnodeQueue
里保存的钩子函数依次执行一遍,它的定义在 src/core/vdom/patch.js
中:
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
该函数会执行 insert
这个钩子函数,对于组件而言,insert
钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks
中:
const componentVNodeHooks = {
// ...
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
// ...
},
}
我们可以看到,每个子组件都是在这个钩子函数中执行 mouted
钩子函数,并且我们之前分析过,insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted
钩子函数的执行顺序也是先子后父。
beforeUpdate & updated
顾名思义,beforeUpdate
和 updated
的钩子函数执行时机都应该是在数据更新的时候,到目前为止,我们还没有分析 Vue 的数据双向绑定、更新相关,下一章我会详细介绍这个过程。
beforeUpdate
的执行时机是在渲染 Watcher 的 before
函数中,我们刚才提到过:
xport function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
注意这里有个判断,也就是在组件已经 mounted 之后,才会去调用这个钩子函数。
update
的执行时机是在flushSchedulerQueue
函数调用的时候, 它的定义在 src/core/observer/scheduler.js
中:
function flushSchedulerQueue () {
// ...
// 获取到 updatedQueue
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
flushSchedulerQueue
函数我们之后会详细介绍,可以先大概了解一下,updatedQueue
是 更新了的 wathcer
数组,那么在 callUpdatedHooks
函数中,它对这些数组做遍历,只有满足当前 watcher
为 vm._watcher
以及组件已经 mounted
这两个条件,才会执行 updated
钩子函数。
我们之前提过,在组件 mount 的过程中,会实例化一个渲染的 Watcher
去监听 vm
上的数据变化重新渲染,这断逻辑发生在 mountComponent
函数执行的时候:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 这里是简写
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
那么在实例化 Watcher
的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher
的实例赋值给 vm._watcher
,定义在 src/core/observer/watcher.js
中:
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
}
}
同时,还把当前 wathcer
实例 push 到 vm._watchers
中,vm._watcher
是专门用来监听 vm
上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
,因此在 callUpdatedHooks
函数中,只有 vm._watcher
的回调执行完毕后,才会执行 updated
钩子函数。
beforeDestroy & destroyed
顾名思义,beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,组件的销毁过程之后会详细介绍,最终会调用 $destroy
方法,它的定义在 src/core/instance/lifecycle.js
中:
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
beforeDestroy
钩子函数的执行时机是在 $destroy
函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent
的 $children
中删掉自身,删除 watcher
,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy
钩子函数。
在 $destroy
的执行过程中,它又会执行 vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样。
activated & deactivated
activated
和 deactivated
钩子函数是专门为 keep-alive
组件定制的钩子,我们会在介绍 keep-alive
组件的时候详细介绍,这里先留个悬念。
最后是官网提供的生命周期图