30 秒速记
- 生命周期标记初始化、挂载、更新与卸载阶段
Vue 2的created可用状态,节点要等挂载- 更新后读取页面,使用对应更新时机
- 定时器、监听和外部实例成对清理
- 缓存停用与真正卸载分开处理
生命周期告诉你组件走到哪一步,适合做什么,而不是提供几处随便塞代码的位置。 在 Vue 2 中,状态初始化后可以读取数据,但操作已挂载节点要等相应挂载阶段;数据改变后的页面读取也要等刷新完成。建立的订阅、定时器和第三方实例需要成对清理。使用缓存组件时,离开画面不一定已经卸载,要区分停用与销毁。
版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。
# 基础知识
# 钩子函数
其实和回调是一个概念,当系统执行到某处时,检查是否有
hook(钩子),有的话就会执行回调。
通俗的说,hook就是在程序运行中,在某个特定的位置,框架的开发者设计好了一个钩子来告诉我们当前程序已经运行到特定的位置了,会触发一个回调函数,并提供给我们,让我们可以在生命周期的特定阶段进行相关业务代码的编写

总的来说,Vue的生命周期可以分为以下八个阶段
beforeCreate实例创建前created实例创建完成beforeMount挂载前mounted挂载完成beforeUpdate更新前updated更新完成beforeDestory销毁前destoryed销毁完成
# beforeCreate
这个钩子是
new Vue()之后触发的第一个钩子,在当前阶段中data、methods、computed以及watch上的数据和方法均不能被访问。
# created
这个钩子在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发
updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果你非要想,可以通过vm.$nextTick来访问Dom。
# beforeMount
这个钩子发生在挂载之前,在这之前
template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated
# mounted
这个钩子在挂载完成后发生,在当前阶段,真实的
Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。也可以向后台发送请求,拿到返回数据
# beforeUpdate
这个钩子发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染
# updated
这个钩子发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新
# beforeDestroy
这个钩子发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
# destroyed
这个钩子发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁
注意点
在使用生命周期时有几点注意事项需要我们牢记
1.除了beforeCreate和created钩子之外,其他钩子均在服务器端渲染期间不被调用。
2.上文曾提到过,在updated的时候千万不要去修改data里面赋值的数据,否则会导致死循环。
3.Vue的所有生命周期函数都是自动绑定到this的上下文上。所以,你这里使用箭头函数的话,就会出现this指向的父级作用域,就会报错。原因下面源码部分会讲解
# 源码解读
// src/core/instance/lifecycle.js
// callhook 函数的功能就是在当前vue组件实例中,调用某个生命周期钩子注册的所有回调函数。
// vm:Vue实例
// hook:生命周期名字
export function callHook (vm: Component, hook: string) {
pushTarget()
const handlers = vm.$options[hook]
// 初始化合并 options 的过程 、,将各个生命周期函数合并到 options 里
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
// src/core/util/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
我们从上面的代码中可以看到
callHook中调用了invokeWithErrorHandling方法,在invokeWithErrorHandling方法中,使用了apply和call改变了this指向,而在箭头函数中this指向是无法改变的,所以我们在编写生命周期函数的时候不能使用箭头函数
# beforeCreate和created
// src/core/instance/init
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
// 合并选项部分已省略
initLifecycle(vm)
// 主要就是给vm对象添加了 $parent、$root、$children 属性,以及一些其它的生命周期相关的标识
initEvents(vm) // 初始化事件相关的属性
initRender(vm) // vm 添加了一些虚拟 dom、slot 等相关的属性和方法
callHook(vm, 'beforeCreate') // 调用 beforeCreate 钩子
//下面 initInjections(vm) 和 initProvide(vm) 两个配套使用,用于将父组件 _provided 中定义的值,通过 inject 注入到子组件,且这些属性不会被观察
initInjections(vm)
initState(vm) // props、methods、data、watch、computed等数据初始化
initProvide(vm)
callHook(vm, 'created') // 调用 created 钩子
}
}
// src/core/instance/state
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)
}
}
- 我们可以看到
beforeCreate钩子调用是在initState之前的,而从上面的第二段代码我们可以看出initState的作用是对props、methods、data、computed、watch等属性做初始化处理。 - 通过阅读源码,我们更加清楚的明白了在beforeCreate钩子的时候我们没有对
props、methods、data、computed、watch上的数据的访问权限。在created中才可以
# beforeMount和mounted
// mountComponent 核心就是先实例化一个渲染Watcher
