再谈Vue的生命周期----结合Vue源码
简介
关于Vue的生命周期函数,目前网上有许多介绍文章,但也都只是分析了表象。这篇文档,将结合Vue源码分析,为什么会有这样的表象。
Vue中的生命周期函数也可以称之为生命周期钩子(hook)函数,在特定的时期,调用特定的函数。
随着项目需求的不断扩大,生命周期函数被广泛使用在数据初始化、回收、改变Loading状态、发起异步请求等各个方面。
而Vue实例的生命周期函数有beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestpry、destroyed,8个。
本文假设读者使用过Vue.js,但对相应的开发经验不做要求。如果你对Vue很感兴趣,却不知如何下手,建议你先阅读官方文本
资源
以下是这篇文章所需的资源,欢迎下载。
表象
我们在该页面来研究,Vue的生命周期函数究竟会在何时调用,又会有什么不同的特性。强烈建议你直接将项目仓库克隆至本地,并在真机环境中,跑一跑。Vue.js已经添加在版本库中,因此你不需要添加任何依赖,直接在浏览器中打开lifeCycle.html
即可。
$ git clone https://github.com/AmberAAA/vue-guide
编写实现组件
定义template
与data
,使其在可以在屏幕实时渲染出来。
{ //... template: ` <div> <p v-html="info"></p> </div> `, data () { return { info: '' } } //... }
以beforeCreate
为例,定义全部的证明周期函数。
beforeCreate() { console.group('------beforeCreate------'); console.log('beforeCreate called') console.log(this) console.log(this.$data) console.log(this.$el) this.info += 'beforeCreate called <br>' console.groupEnd(); }
屏幕输出
在浏览器中打开lifeCycle.html
,点击挂载组件后,屏幕依次输出created called
、beforeMount called
、mounted called
。表现出,在挂载组件后,info
被created
, beforeMount
, mounted
赋值,并渲染至屏幕上。但是本应在最开始就执行的beforeCreate
却并没有给info
赋值。
卸载组件时,因为程序运行太快,为了方便观察,特意为beforeDestroy
和beforeDestroy
函数在最后添加了断点。发现点击卸载组价后,Vue在v-if=true
时会直接从文档模型中卸载组件(此时组件已经不在document)。
控制台输出
控制台输出的内容非常具有代表性。
我们可以发现,控制台按照创建、挂载、更新、销毁依次打印出对应的钩子函数。展开来看
在触发beforeCreate
函数时,vue实例还尚未初始化$data
,因此也就无法给$data
赋值,也就很好的解释了为什么在屏幕上,没有渲染出beforeCreate called
。同时,因为尚未被挂载,也就无法获取到$el
。
在触发created
函数时,其实就表明,该组件已经被创建了。因此给info
赋值后,待组件挂载后,视图也会渲染出created called
。
在触发beforeMount
函数时,其实就表明,该组件即将被挂载。此时组建表现出的特性与created
保持一致。
在触发mounted
函数时,其实就表明,该组件已经被挂载。因此给info
赋值后,待组件挂载后,视图也会渲染出mounted called
,并且控制台可以获取到$el
触发beforeUpdate
与updated
,分别表示视图更新前后,更新前$data
领先视图,更新后,保持一致。在这两个回调函数中,更改data时注意避免循环回调。
触发beforeDestroy
与destroyed
,表示实例在销毁前后,改变$data
,会触发一次updated
,因在同一个函数中(下文会介绍)回调,故捏合在一起说明。
名称 | 触发阶段 | $data | $el |
---|---|---|---|
beforeCreate | 组件创建前 | ✖ | ✖ |
created | 组件创建后 | ✔ | ✖ |
beforeMount | 组件挂载前 | ✔ | ✖ |
mounted | 组件挂载后 | ✔ | ✔ |
beforeUpdate | 组件更新前 | ✔ | ✔ |
updated | 组件更新后 | ✔ | ✔ |
beforeDestroy | 组件创建前 | ✔ | ✔ |
destroyed | 组件创建前 | ✔ | ✔ |
原理
Vue生命周期函数在源码文件/src/core/instance/init.js
中定义,并在/src/core/instance/init.js
、src/core/instance/lifecycle.js
、/src/core/observer/scheduler.js
三个文件中调用了所有的生命周期函数
callHooK
当在特定的使其,需要调用生命周期钩子时,源码只需调用callHook函数,并传入两个参数,第一个为vue实例,第二个为钩子名称。如下
export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() const handlers = vm.$options[hook] if (handlers) { //? 这里为什么是数组?在什么情况下,数组的索引会大于1? 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在打包时,并没有暴露在全局作用域。但我们可以根据Vue实例来手动调用生命周期函数。试着在挂在组件后在控制台输入vue.$children[0].$options['beforeCreate'][0].call(vue.$children[0])
,可以发现组件的beforeCreate钩子已经被触发了。并且表示出了与本意相驳的特性。此时因为组件已经初始化,并且已经挂载,所以成功在控制台打印出$el
与$data
,并在修改info
后成功触发了beforeUpdate
与beforeUpdate
beforeCreate
与created
Vue会在/src/core/instance/init.js
中通过initMixin
函数对Vue实例进行进一步初始化操作。
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { /* .... */ vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) // 定义$data initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* ... */ } }
可以看出在执行callHook(vm, 'beforeCreate')
之前,Vue还尚未初始化data,这也就解释了,为什么在控制台beforeCreate获取到的$data
为undefined
,而callHook(vm, 'created')
却可以,以及屏幕上为什么没有打印出beforeCreate called
。
beforeMount
与mounted
Vue在/src/core/instance/lifecycle.js
中定义了mountComponent
函数,并在该函数内,调用了beforeMount
与mounted
。
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 组件挂载时 `el` 为`undefined` callHook(vm, 'beforeMount') // 所以获取到的`$el`为`undefined` /* ... */ // 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 //! 挖个新坑 下节分享渲染watch。 经过渲染后,即可获取`$el` new Watcher(vm, updateComponent, noop, null, 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 // 因为已经渲染,`$el`此时已经可以成功获取 callHook(vm, 'mounted') } return vm }
beforeUpdate
与updated
beforeUpdate
与updated
涉及到watcher,因此将会在以后的章节进行详解。
beforeDestroy
与destroyed
Vue将卸载组件的方法直接定义在原型链上,因此可以通过直接调用vm.$destroy()
方法来卸载组件。
Vue.prototype.$destroy = function () { const vm: Component = this if (vm._isBeingDestroyed) { return } // 吊起`beforeDestroy`钩子函数 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 // 调起`destroyed`钩子函数 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 } } }
问题
vue.$children[0].$options['beforeCreate']
为什么是一个数组?- 卸载组件,会触发一个
updated called
? new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
这行代码之后发生了什么?beforeUpdate
背后实现原理。