Vue源码解读:05生命周期篇
目录
第一节·先看目录结构
├─instance # 实现vue实例的代码
│ ├─render-helpers # render辅助代码
│ ├─events.js # 事件初始化处理相关代码
│ ├─index.js #
│ ├─init.js # 初始化代码
│ ├─inject.js #
│ ├─lifecycle.js # 生命周期相关代码
│ ├─proxy.js # 代理相关代码
│ ├─render.js # render渲染函数相关代码
│ └─state.js #
第二节·Vue生命周期简述
我不太敢写什么是生能周期这个定义,总感觉压力很大,虽然只是笔记,哈哈哈。在学校上C语言的必修课的时候,似乎没听说过生命周期这个说话,但从java开始,很多技术栈,也不知道是个啥东东,就老爱有生命周期这个东东。反正不管啥,生命周期应该也许大概可能就是从开始到结束的过程,前提是支持周期的。了解生命周期的意义应在于可以帮助我们写出更优雅的代码,以及快速定位bug,而不是纠结于定义本身。知道vue的生命周期中都干了啥,啥时候干的,是我们了解vue生周期核心所在。
第一段就当做是日记,下面开始正经的笔记。vue实例开始创建直至被销毁的过程,称为vue的生命周期。vue的生命周期依次,主要经历初始化,模板编译,挂载,销毁四个阶段(官网vue实例生命期图示底部提示了一句话,模板编译在使用构造生成文件中,将提前执行。)。与vue生命周期密切相关的钩子函数有beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy,destroyed。vue的api文档上,一共有11个和生命周期有关的钩子函数,另外三个分别是activated,deactivated,最后一个是vue2.5.0新增的,叫errorCaptured。
那么来一张官方的生命周期图。
第三节·生命周期的几个阶段
1.生命周期中的几个阶段。
从官网的生命周期图和源码看,可以大致将vue的生命周期分为4个阶段,分别是初始化阶段,模板编译阶段,挂载阶段,销毁阶段。
初始化阶段:为vue实例初始化属性、事件、数据观测等。
模板编译阶段:将模板字符串编译成渲染函数。(该阶段在runtime版本中不存在,因为已经编译好了。)
挂载阶段:将vue实例挂载到指定dom上,即将模板渲染到真实dom中。
销毁阶段:解绑指令,移除事件监听器,销毁子实例。
2.初始化阶段
初始化阶段主要干了两件事,一是创建Vue实例,二是为实例初始化属性、事件、响应式数据。
从instance的入口文件index.js可以看到,Vue函数代码很少,核心代码就一行即调用实例的_init方法,如下:
//源码位置 src/instance/index.js
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)
}
//源码位置 src/instance/init.js
而_init方法是在initMixin函数中被挂载到Vue原型上的。initMixin代码如下:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 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) //初始化事件
initRender(vm) //初始化渲染
callHook(vm, 'beforeCreate') //触发beforeCreate钩子
initInjections(vm) // resolve injections before data/props
initState(vm) //初始化状态,例如props,methods,data,computed,watch
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') //触发created钩子
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el) //调用$mount,进入下一阶段:挂载阶段或模板编译阶段
}
}
}
从源码中可以看到,在init初始化中,初始化了生命周期,事件,render渲染,触发beforeCreate钩子,实例状态(props,methods,data,computed,watch),触发created钩子等。从这里我们可以得到一个信息,created钩子的时候,实例已经被初始化,且props,methods,data,computed,watch可用;因为触发beforeCreate钩子在初始化实例状态之前,所以按理beforeCreate钩子中是无法使用实例的props,methods,data,computed,watch属性的。
//源码位置 src/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)//初始化Props属性
if (opts.methods) initMethods(vm, opts.methods)//初始化Methods属性
if (opts.data) {
initData(vm) //初始化data属性
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) //初始化Computed计算属性
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)//初始化Watch监听属性
}
}
关注一下initState(vm) 初始化状态,这个函数,里面核心代码如下,从中我们可以得到一个信息,vue实例的属性在初始化的时候的执行顺序依次是props,methods,data,computed,watch。
初始化阶段通过callHook触发的生命周期钩子是beforeCreate,created。
3.模板编译阶段
在初始化函数_init中,在调用一系列初始化函数后,即初始化工作完成后之后,在改该函数的末尾,调用了$mount,使vue生命周期进入下一个阶段。这里的下一个阶段是模板编译阶段,当然这个阶段不一定存在,有可能初始化阶段完成之后就直接跳到挂载阶段了。
模板编译阶段的主要内容是将模板字符串转换成render函数,在vue的某些版本比如vue.runtime.js,就不会有模板编译阶段。模板编译阶段的之所以有可能不存在,是因为某些情况下,模板字符串可能已经提前编译了,比如vue-loader就有预编译的功能。事实上,我们的日常开发中,使用.vue的文件内部的模板字符串,都是在构建的时候就已经预编译了。
4.挂载阶段
瞅一眼官方的生命周期图示,大概就可以知道挂载阶段主要工作一是创建创建Vue实例并用其替换el选项对应的DOM元素,二是开启数据状态的监控(如何监控的详看变化侦测篇)。挂载阶段有关的钩子函数有beforeMount,beforeUpdate,mounted,updated。
关于挂载阶段beforeMount,beforeUpdate,mounted的触发时机,可以看下lifecycle.js文件中的mountComponent函数,该函数是在进入挂载阶段时调用的。
// 源码位置 src/instance/lifecycle.js
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
)
}
}
}
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
}
5.销毁阶段
vue实例生命周期的最后一个阶段,销毁阶段,该阶段的主要工作是销毁,比如解绑指令,销毁子组件,取消依赖最终,销毁事件监听器。从上图可以看出,当调用vm.destroy函数的源码如下。
//源码位置 src/instance/lifecycle.js
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy') //触发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') //触发destroyed钩子
// turn off all instance listeners.
vm.$off() //解绑所有的实例监听器
// remove __vue__ reference
//移除ref引用
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
//将vue实例从父级中解绑
}
}
由源码看到,在销毁阶段触发钩子函数有beforeDestroy,destroyed。beforeDestroy是在比较靠前的位置,这时vue实例还没被销毁,即是说vue实例还是可以正常使用的。我们想要在实例销毁前做的事情,可以再beforeDestroy钩子中写,比如销毁定时器等。
第四节·有关执行顺序
1.几个生命周期钩子的运行顺序,依次先后是beforeCreate,created,beforeMount,mounted,beforeDestroy,destroyed。beforeUpdate,updated的话,dom只要有更新,他们就会运行,可以确定的只能是beforeUpdate必定在updated之前运行。
2.几个内置属性的执行顺序依次是props,methods,data ,computed,watch。
3.父子组件中几个钩子的执行顺序。父组件是子组件的容器,从这角度理解,那么应当是父组件先创建,然后才能容纳子组件创建。而挂载渲染,子组件作为父组件一部分,父组件的挂载渲染完成,应当以子组件为前提。(特例除外)
所以 子mounted 先于父mounted执行,父created 先于 子created执行。
加载渲染:
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate
-> 子created -> 子beforeMount -> 子mounted -> 父mounted
子组件更新:
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
父组件更新:
父beforeUpdate -> 父updated
销毁:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
第五节·几个生命周期钩子
// 源码位置 src/shared/constants.js
// 2.6.11版本的vue
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
研究vue版本是2.6.11,如上代码,我们可以看到一共注册了12钩子。
serverPrefetch:最后一个serverPrefetch不了解,我猜是和vue支持ssr服务端渲染有关。
errorCaptured:倒数第二个errorCaptured是在2.5.0+ 新增,官网关于它的介绍是:
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:
错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。
此钩子可以返回 false 以阻止该错误继续向上传播。
activated:被 keep-alive 缓存的组件激活时调用。(在服务器端渲染期间不被调用)
deactivated:被 keep-alive 缓存的组件停用时调用。(在服务器端渲染期间不被调用)
beforeCreate:初始化阶段,vue实例的挂载元素$el和数据对象data都为undefined,还不可用。
created:初始化阶段,data,methods已经初始化,可用。$el不可用。
beforeMount:挂载阶段,实例挂载前,此时按理el已经初始化,dom节点还是虚拟dom节点。
mounted:挂载阶段,此时实例已经完成挂载,$el可用。
beforeUpdate:有更新才会触发,data被修改时,dom更新前触发。
updated:有更新才会触发,dom更新时触发。
beforeDestroy:销毁阶段,实例销毁前执行,此时vue实例还可正常使用。
destroyed:销毁阶段,据官网实例销毁后调用。此时this指向的实例还存在,但是子实例,监听器等已经被销毁。
我做了一个简单的打印验证,打印结果和代码如下:
<template>
<div>
<div>{{name}}</div>
<div @click="to">跳转到别的页面</div>
<tab-bar-bottom ref="child"></tab-bar-bottom>
</div>
</template>
<script>
export default {
name: "withdrawal",
methods:{
to(){
this.$router.push({path:'/'}).then()
}
},
data() {
return {
name: 'hello world'
}
},
beforeCreate() {
console.log('==============' + 'beforeCreated' + '===================')
console.log(this.$el) //undefined
console.log(this.$data) //undefined
console.log(this.name) //undefined
console.log(this.$refs.child) //undefined
},
created() {
console.log('==============' + 'created' + '===================')
console.log(this.$el) //undefined
console.log(this.$data) //有值
console.log(this.name) //有值
console.log(this.$refs.child) //undefined
},
beforeMount() {
console.log('==============' + 'beforeMount' + '===================')
console.log(this.$el) //undefined
console.log(this.$data) //有值
console.log(this.name) //有值
console.log(this.$refs.child) //undefined
},
mounted() {
console.log('==============' + 'mounted' + '===================')
console.log(this.$el) //有值
console.log(this.$data) //有值
console.log(this.name) //有值
console.log(this.$refs.child) //有值
},
beforeUpdate() {
console.log('==============' + 'beforeUpdate' + '===================')
console.log(this.$el) //点击更新name后进入,且有值
console.log(this.$data) //点击更新name后进入,且有值
console.log(this.name) //点击更新name后进入,且有值
console.log(this.$refs.child) //点击更新name后进入,且有值
},
updated() {
console.log('==============' + 'updated' + '===================')
console.log(this.$el) //点击更新name后进入,且有值
console.log(this.$data) //点击更新name后进入,且有值
console.log(this.name) //点击更新name后进入,且有值
console.log(this.$refs.child) //点击更新name后进入,且有值
},
beforeDestroy() {
console.log('==============' + 'beforeDestroy' + '===================')
console.log(this.$el) //有值
console.log(this.$data) //有值
console.log(this.name) //有值
console.log(this.$refs.child) //有值
console.log(this) //有值
},
destroyed() {
console.log('==============' + 'destroyed' + '===================')
console.log(this.$el) //有值
console.log(this.$data) //有值
console.log(this.name) //有值
console.log(this.$refs.child) //undefined
console.log(this) //有值
}
}
</script>
第六节·篇章小结
本篇章小结如下:
①1个定义:vue实例开始创建直至被销毁的过程,称为vue的生命周期。
②4个阶段:介绍了初始化、模板编译、挂载、销毁四个阶段。
③12个钩子:简单介绍了12个钩子函数:beforeCreate,created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy,destroyed, activated,deactivated, errorCaptured, serverPrefetch。
④2个排序:一是几个钩子执行顺序依次为beforeCreate,created,beforeMount,mounted,beforeDestroy,destroyed。beforeUpdate,updated则是dom有要更新才会出发。二是vue实例几个属性的执行顺序依次是props,methods,data ,computed,watch。