今天想来总结一些关于生命周期的知识,其实一开始看生命周期的时候是一头雾水的,但是Vue官网有句关于生命周期的话说的好:“你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。”而现在经过了一段时间的学习,也有了一些理解了,所以写下来供以后回忆。
首先什么是Vue的生命周期呢?就和人也有自己的一生一样(从刚出生一直成长,然后长大,最后老去),Vue同样如此,它也有自己的一生,而生命周期就是指“一个Vue实例(就是通过new Vue()创建的一个对象)从创建到销毁的整个过程”。
而Vue的生命周期中有很多个关键的节点,也就像人一样,比如说我高考,参加工作之类的。而这些Vue这些关键的节点都会触发一个函数,我们叫做生命周期函数,也就是钩子。现在就让我把官网的生命周期图示贴在下方(建议大家边看边把这个图放在旁边):
所以现在就让我们根据这个生命周期官网图示来一步步理解一下Vue的生命周期:
一、首先我们来看beforeCreate生命周期函数:
我们可以在图解中看到,最开始有两个函数,一个是beforeCreate函数,一个是created函数,显然beforeCreate函数是在created函数之前(毕竟是beforeCreate哈哈哈),那么我们看图,beforeCreate函数之前做了什么呢,我们可以看到,首先肯定是创建一个Vue实例(new Vue),在创建了一个Vue实例后,Vue执行init Events & Lifecycle,init Events & Lifecycle用中文翻译就是初始化事件和生命周期,那初始化事件和生命周期又是干什么呢?
其实这是两个函数:一个是initEvents和initLifecycle,大概流程如图:
那这两个函数又执行了什么呢?
1.首先我们来看initEvents函数,它的源码如下,感觉各个版本的源码还是有点点差别,但是重要的思想:
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
大概步骤如下:
(1)可以看到函数中用 vm._events = Object.create(null) 这句话初始化了一个空对象,是用来存放父组件绑定在当前组件的事件的。
(2)然后又 vm._hasHookEvent = false 初始化vm._hasHookEvent的值为false,表示父组件是否通过@hook把钩子绑定在该组件上。
(3)然后就是 const listeners = vm.$options._parentListeners 追踪父组件注册的事件然后赋值给listeners,然后判断如果listerners存在,就调用updateComponentListeners(vm, listeners) 将父组件注册的事件更新到子组件里来,而updateComponentListeners(vm, listeners)中其实最主要的实现功能的函数就是updateListeners(listeners, oldListeners || {}, add, remove, vm),而它的主要作用就是比较listeners和oldListeners的不同,然后传入add和remove对事件进行操作(老的没有,新有就add,老的有,新的没有就remove),最后将更新之后的父组件在子组件注册的事件放入_events对象中。
这是initEvents()大概的流程,如果想更深入了解的可以去网上直接搜索initEvents()。
2.再来看initLifecycle函数,源码如下,因为没那么复杂,解释就直接写在注释里面:
export function initLifecycle (vm: Component) {
//把合并后的options赋值给options变量。
const options = vm.$options
//将vm中的父实例赋给parent变量
let parent = options.parent
//如果parent存在且本实例不为抽象组件(抽象组件就是缓存的不活动的组件实例(keep-alive))
if (parent && !options.abstract) {
//如果父实例时抽象组件则接续找爷爷
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
//找到了就将vm push到父实例的children对象里面
parent.$children.push(vm)
}
//将父实例赋值放到vm的$parent对象里面
vm.$parent = parent
//如果vm没有父实例 那root就是他自己
vm.$root = parent ? parent.$root : vm
//之后就是初始化一些需要的对象和生命周期
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
大概步骤如下:
(1)把合并后的options赋值
(2)通过options.parent一直往上找实例的父实例,有的话则把vm push到父实例的children对象里面,然后把父实例放到vm的parent对象里面。
(3)初始化自己的children对象,还有一些钩子函数的状态。
注:还有一点想说的,就是可以看到,源码中initLifecycle是在initEvents之前的,个人理解initLifecycle先初始化了它的options参与和父实例,这样才为initEvents初始化父组件绑定在当前组件的事件做了准备,所以必须先initLifecycle再initEvents,但是我们这里先讲intiEvents是因为生命周期图示上写的init Events & Lifecycle(点题哈哈哈)
3.虽然生命周期图示中没有出现initRender,但是源码中出现了,我们就说一说:
initRender字面意思一看就知道是渲染初始化,我们知道,真正开始渲染工作是要到mount相关钩子函数去了,那它总要初始化是吧,所以就在beforeCreate完成渲染初始化。源码如下:
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
说实话,看到这么多我就头晕了,所以我们提取一下关键步骤。
大概流程如下:
(1)先把合并后的options赋值,然后找到父组件的节点parentVnode和渲染内容renderConstext,然后初始化插槽slot,
(2)createElement函数(用来生成虚拟 DOM 树的函数)挂载到 vm
的 _c
和 $createElement
两个属性上(参数不同)
(3)把父组件的$attrs和$listeners用 defineReactive()(也就是Vue实现数据监听以及 单向数据流的主要方式,之后我也要具体去学习一下)设置为响应式的,$attrs和$listeners是什么?如下图:
到此,beforeCreate函数之前的工作全部都做完了。总的来说,beforeCreate之前完成initLifecycle、initEvents、initRender这三个事情。
二、继续往下走,来到beforeCreate之后、created钩子函数之前,那么created函数之前Vue又做了什么呢?
可以通过图解看到,vue执行了init injections & reactivity,中文翻译过来是初始化注入和响应式,那么我们再看一次这个流程图:
可以看到,vue执行了三个函数initInjections、initState、initProvide,我们可以合理的推测,一定是这三个函数一起完成的注入和响应式的初始化,那我们来看看这三个函数到底干了什么吧。
1.initInjections和initProvide
为什么把这两个函数放一起呢?因为源码里面他们就在一个文件(inject.js)里面,我觉得在看initInjecttions和initProvide函数之前,先要知道inject和provide是什么(我一开始也不知道,然后去官网看了下,如下图)
解释:
示例:
然后我们再来看下源码:
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
initProvide源码比较简单,就是将options里面的provide对象赋值给_provided
initInjections就复杂一些了,大概步骤如下:
(1)用resolveInject()函数将inject中的变量转换为健值对赋值给result,实例如下:
这个实例来自 juejin.cn/post/687778… 这篇文章.
// 父级组件提供 'foo'
var Parent = {
provide: {
foo: 'bar'
}
}
// 子组件注入 'foo'
var Child = {
inject: ['foo'],
}
// result
result = {
'foo':'bar'
}
(2)然后遍历result,将每个键值对用defineReactive()函数绑定到实例,但是不是响应式,因为toggleObserving(false)这句,意思就是单纯的绑定,不加响应式,官网是这样说的:
2.经过以上步骤我们的initInjections和initProvide就操作完了,但是不要忽视他们中间还夹了一个initState:
首先它为啥是被夹在中间呢?我们先来看看initState的源码:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props)
initProps(vm, opts.props)
if (opts.methods)
initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
这里还是比较清晰的,可以看出来它初始化了很多东西:props,methods,data,computed,watch,这都是大家平常经常看到的属性呀!所以我们回到那个问题,为啥它被夹在中间呢,看官网实例:
这里的data就包括在initState里面了,所以在initInjections对inject里面的数据处理完之后要先initState(因为inject的数据就在data里面被使用了),再initProvide。
那现在我们回到生命周期图示,图示上说created之前vue执行了init injections & reactivity,讲的这里我们也只看到init injections,这个init reactivity又体现在哪里呢?
我们推理一下,前面在init injections和 init provide里面我们确实看到defineReactive()函数,不过在使用时用了toggleObserving(false)来故意设置不响应,所以响应肯定时在initState()函数里面的,那么有了方向我们就去找:
直接点开initData这个函数,因为data里面的数据肯定要让他响应式是吧,然后我们看下initData()的源码:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
代码好长,我看了一下,有点晕,但是可以大概知道就是拿到props还有methods还有data的keys然后循环判断一些条件,然后做相应的处理和报错提示,但是最重要的肯定式在observe里面,也就是这句代码 observe(data, true ) 我记得我之前在b站上看到一个视频,说的就是双向绑定都有一个observe类(观察者)来监听,那我们点进去看看。
哭了,感觉真的深度优先搜索学习,observe()源码如下:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
也可以大概看一下,这个函数一进来,就先判断传进来的data是不是对象,不是则直接返回。然后再判对象上石否有__ob__属性,有就直接赋值__ob__,防止重复监听,如果没有被监听就直接走ob = new Observer(value)这个构造函数,把data也传过去,想必肯定是传过去监听啊,我们马上点过去!
那么我们在看Observe(value)做了啥,继续源码如下!:(我们只看这个构造函数的constructor)
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
可以看出这个构造函数,先创建了三个属性,一个value,一个dep,一个vmCount,最后一个估计是个计数器,不管它了,那我在看这个def(value, 'ob', this),可以合理推测,肯定是给value加个了__ob__属性来代表它已经被监听了,和前面observe函数那个判断是否存在__ob__属性对上了!
然后再判断是否为数组,如果是数组则this.observeArray(value)来监听数组。如果是对象则this.walk(value)来监听data,我看了下这个函数,就是循环对象的keys,然后用defineReactive()设置响应式!
到这里,我们对initData粗略的逻辑就大概理清楚了,也终于知道init reactivity体现在哪了吧!真是辛苦!所以到这里,created之前的所以准备工作也都做好了!
我们大概总结一下:beforeCreate之前vue执行了init Lifecycle,init Events,init render。然后在created的之前init injections,init provide,init state(这里面初始化了一些常用的属性,其中initData里面体现了init reactivity),所以beforeCreate之前相当于在为后面的生命周期进行做准备,然后created之后就会完成一些常用属性的初始化,已经data数据的响应式绑定。也就是说在created之后就可以在vue中用this.data调用data里面的数据了!
三、beforeMount
完成initLifecycle,initEvents,iniRender,initInjections,initState,initProvide之后,我们就要开始准备渲染了,让我们继续看生命周期的图示.
Has “el” option, 这里vue就进行了判断,看el是否挂载了东西,在源码中体现在这里:
可以看到,如果vm.$options.el存在,也就是el挂载了东西,那我们就运行这个$mount函数,并把el传进去。那这个$mount函数又在哪呢?首先讨论这个$mount要分两个版本,runtime only和runtime+compile,而runtime+compile版本需要经过编译生成render。我们就来讨论runtime+compile版本,这样有助于我们对原理的深入学习。runtime+compile版本的$mount函数的源码如下:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
它缓存了原型的$mount方法,再重写了这个方法,在里面加入了编译生成render的部分。
原型的方法如下:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
可以看出compile版本的$mount函数的上半部分的代码进行了tempalte的判断,下部分通过compileToFunctions对tempalte进行处理,将其编译成render函数。完美的契合了生命周期图示中的如下部分。
完成这些之后就return mountComponent(this, el, hydrating)里去了,所以我们再去找下mountComponent(this, el, hydrating)这个函数,源码如下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
//beforeMount在这里!!!
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 && !vm._isDestroyed) {
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
}
可以看到代码上半部分,进行了一些判断之后就直接callHook(vm, 'beforeMount'),这里就意味着beforeMount之前的工作就已经完成了!
那我们来总结一下,created之后,就会先判断el是否挂载,然后就调用$mount函数,然后如果是runtime+compile版本的话,就会判断是否有tempalte,如果有就用compileToFunctions函数将其编译成render函数,如果没有template就将el挂载的外部html编译成render函数。
注:1.这时候有了render,但是还没生成虚拟DOM,所以这个时候打印的el还是原来的,没有渲染之前的el。2.如果el不存在的话,就可以我们自己手动调用这个vm.$mount函数来绑定el来开始渲染.
四、mounted
做好渲染准备了之后,就是要开始生成虚拟DOM,然后再给渲染成真实的DOM了。这些步骤都是在mounted之前完成的,我们接着看mountComponent这个函数,但是是看callHook(vm, 'beforeMount')之后的代码
//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 && !vm._isDestroyed) {
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
主要就是new了一个Watcher,里面有一个回调函数updateComponent(),这个函数就是关键,在updateComponent()函数中会用vm._render
方法先生成虚拟DOM,最终调用 vm._update
更新 成真实DOM。代码like this:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
Watcher的作用顾名思义,就是观察者,就是在初始化的时候updateComponent()一次,然后观察到vm中数据发生变化的时候再执行updateComponent()更新DOM。
然后函数会判断vm.$vnode是否为为空,如果为空,则说明它为根实例,意思就是渲染完了,这个时候就可以调用mounted函数了。
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
所以现在主要就是看vm._update和vm._render到底干了啥了:
vm_render主要就是对render函数的调用(render函数就是前面所说在beforeMount函数之前的阶段由template通过compileToFunctions函数生成的),而render调用时函数的参数是 vm.createElemen?大家应该还有印象,当我们前面在讲initLifecycle时候中讲到vue会把createElement函数挂载到 vm
的 _c
和 $createElement
属性上,而vm._c是在模板编辑成的render中使用,vm.$createElement是在用户手写的render中使用,所以真正创建虚拟DOM的其实是createElement函数(创建了一个VNode Tree).
vm._update它被调用的时机有 2 个,一个是首次DOM,一个是数据发生变化的时候更新DOM,都是把 虚拟DOM转换成真实的DOM,具体的细节我以后再看吧,已经看了三天了,休息一下。
总结一下,1.mounted之后就代表着渲染的完成,已经真实的DOM已经挂载到html上面,此时进行DOM相关操作,如果打印this.el,可以发下此时的el已经是渲染完成的了!2.通过源码可知,在beforeMount和mounted函数之间,vue调用了render,所以我们也可以在beforeMount和mounted生命周期函数之间手写一个render,vue就会执行我们自己的render函数。
写在最后
写了三天,就写了create相关和mount相关的生命周期函数,mounted函数之前的过程还没写详细,之后还会补充的,谢谢大家的观看,如果有不对之错,欢迎大家指正,谢谢!