一、简介
先贴一下官网对生命周期/钩子函数的说明(先贴为敬):所有的生命周期钩子自动绑定 this
上下文到实例中,因此你可以访问数据,对属性和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos()
)。这是因为箭头函数绑定了父上下文,因此 this
与你期待的 Vue 实例不同,this.fetchTodos
的行为未定义。
上面是官方文档对生命周期/钩子函数的总览介绍。如果单看这个总览介绍,绝对是一头雾水,不清不楚看不出个所以然。虽然文档后面对各个钩子函数的使用有具体说明,但具体实例却不是很清楚,所以在玩了一段时间的Vue项目后,闲来打算自己总结下生命周期和钩子函数的使用。下面先来一张官方生命周期图示:
生命周期:描述Vue实例或组件从创建到销毁(包括销毁前和销毁)的全部经历和过程。就像人一样,从母亲怀胎开始,然后出生,成长,衰老,一直到回光返照(销毁前),最后死去一把火(销毁)回归大自然,着重是介绍一种经历和过程。
钩子函数:钩子函数则是Vue实例或组件在生命周期过程中各个阶段自执行的回调函数。就如同新生儿出生后,饿了他会哭,上学途中被高年级学生欺负了会找家长告状,长大了要出去挣钱养家,老了会戴老花镜一样。在不同的阶段Vue实例或组件内部,结构也在发生着变化,随着节点结构的变化就需要执行一些特定的钩子函数,去继续下一步变化和新节点的建立,也正是这些钩子函数的执行为实际开发过程中能够添加自定义功能提供了入口。
下面结合官方文档先对各个钩子函数做一个简略的总结。
二、代码实测
各个钩子函数的执行位置以及执行时间点,在上面的官方生命周期图示中已经标注得很清楚,下面通过代码实测来逐个加深认识。测试代码如下:
<!DOCTYPE html> <html> <head> <title>Vue – 基础学习(1):对生命周期和钩子函的理解</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div id="app"> <div>静态元素</div> <div style="margin-top: 5px">{{ testInfor }}</div> <div style="margin-top: 20px"> <button @click.stop="editTestInfor">更新内容</button> <button @click.stop="destroyedNode">销毁实例</button> </div> </div> <script type="text/javascript" src="https://cdn.bootcss.com/vue/2.5.20/vue.min.js"></script> <script type="text/javascript"> new Vue({ el: '#app', data() { return { testInfor: '测试信息!', }; }, beforeCreate() { console.group('beforeCreate:实例创建完成,但数据对象data、属性、event/watcher事件均未完成配置和初始化。挂载阶段还未开始,$el属性未初始化,$el元素不可见========》'); console.log(this); // object console.log('%c%s', 'color:red', 'el : ' + this.$el); // undefined console.log(this.$el); // undefined console.log('%c%s', 'color:red', 'data : ' + this.$data); // undefined console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // undefined this.testFuntion('beforeCreate'); // undefined this.testFuntion is not a function debugger; }, created() { console.group('created:实例数据对象data、属性、event/watcher事件均配置和初始化完成。但挂载阶段还未开始,$el属性未初始化,$el元素不可见=========================》'); console.log(this); // object console.log('%c%s', 'color:red', 'el : ' + this.$el); // undefined console.log(this.$el); // undefined console.log('%c%s', 'color:red', 'data : ' + this.$data); // 初始化完成 console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // 初始化完成 this.testFuntion('created'); // event事件初始化完成 debugger; }, beforeMount() { console.group('beforeMount:在开始挂载之前被调用,相关的render函数首次被调用。$el属性初始化完成,但处于虚拟dom状态,具体的data.filter尚未替换,$el元素可见=======》'); console.log(this); // object console.log('%c%s', 'color:red', 'el : ' + this.$el); // $el属性初始化完成 console.log(this.$el); // 节点挂载完成,但数据尚未渲染,处于虚拟DOM状态 console.log('%c%s', 'color:red', 'data : ' + this.$data); // 已被初始化 console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // 已被初始化 this.testFuntion('beforeMount'); // event事件已被初始化 debugger; }, mounted() { console.group('mounted:挂载完成,data.filter成功渲染,页面整体渲染完成,可进行DOM操作===================》'); console.log(this); // object console.log('%c%s', 'color:red', 'el : ' + this.$el); // $el属性已被初始化 console.log(this.$el); // 节点挂载完成,数据渲染成功,页面全部渲染完成 console.log('%c%s', 'color:red', 'data : ' + this.$data); // 已被初始化 console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // 已被初始化 this.testFuntion('mounted'); // event事件已被初始化 debugger; }, beforeUpdate() { console.group('beforeUpdate:页面依赖的参数数据更改之后,DOM结构重新渲染之前触发执行(此时DOM结构还没有重新渲染)=============》'); console.log(this.$el); console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // 点击按钮调用方法更新后的数据 this.testFuntion('beforeUpdate'); this.testInfor = 'beforeUpdate修改后的信息!'; // 在beforeUpdate函数内再次修改页面依赖参数数据 console.log('%c%s', 'color:blue', 'data : ' + this.testInfor); // 在beforeUpdate函数内修改后的数据 debugger; }, updated() { console.group('updated:数据更新完成=====================================================================》'); console.log(this.$el); console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // 最终更新后数据 this.testFuntion('updated'); debugger; }, beforeDestroy() { console.group('beforeDestroy:实例或组件销毁之前调用,在这一步,实例或组件仍然完全可用===================》'); console.log(this.$el); console.log('%c%s', 'color:red', 'data : ' + this.testInfor); this.testFuntion('beforeDestroy'); // 此时实例内功能函数功能依然正常 this.testInfor = 'beforeDestroy修改后的信息!'; // 在beforeDestroy函数内再次修改页面依赖参数数据,用以验证beforeUpdate和updated函数是否还监听执行 console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // 在beforeDestroy函数内修改后的数据 debugger; // 此时实例、组件虽然页面结构完整,各种功能正常。但,页面依赖参数更新后生命周期函数beforeUpdate和updated均不再执行,说明实例或组件的销毁一旦启动则不可逆转或中途打断。 }, destroyed() { console.group('destroyed:实例或组件已被销毁=============================================================》'); console.log(this.$el); console.log('%c%s', 'color:red', 'data : ' + this.testInfor); // 在beforeDestroy函数内修改的页面依赖参数,依然能正确读取 this.testFuntion('destroyed'); // 此时实例内功能函数功能依然正常 debugger; // 此时虽然"beforeDestroy"执行完毕,但实例指向的所有东西(参数,方法等)尚未解绑。所以此时实例内各参数、方法功能依然正常。等待"destroyed"执行完毕后,所有的东西才会解绑,尘归尘,土归土。 }, methods: { testFuntion(type) { console.log('当前运行钩子函数:' + type); }, editTestInfor() { this.testInfor = '修改后的信息!'; }, destroyedNode() { this.$destroy(); } } }); </script> </body> </html>
1. beforeCreate 和created
beforeCreate:实例创建完成,但数据对象data、属性、event/watcher事件均未完成配置和初始化。挂载阶段未开始,$el属性尚未初始化,$el属性不可见,$el元素不可见。
created:实例数据对象data、属性、event/watcher事件均配置和初始化完成。但挂载阶段尚未开始,$el属性未初始化,$el属性不可见,$el元素不可见。
小结:虽然此时$el属性尚未初始化,页面元素不可见,但数据对象data、属性、event/watcher事件均已配置和初始化完成,所以一些需要先页面执行的方法(如ajax请求,页面功能权限检测(页面是否能加载、页面依赖参数是否合法)和配置(如按钮点击权限等))在created阶段可以执行,但不允许操作DOM节点和调用操作DOM节点的方法(页面整体结构未渲染完成)。
2. beforeMount和mounted
beforeMount:在开始挂载之前被调用,相关的render函数首次被调用。$el属性初始化完成,$el属性可见,el元素可见。但此时el节点并没有渲染进数据,el节点尚处于“虚拟”节点状态,可看到还是取值表达式{{testInfor }}。这就是Virtual DOM(虚拟Dom)的巧妙之处,先占坑,然后到mounted挂载阶段时再渲染值。
mounted :节点挂载完成,数据成功渲染,页面整体渲染完成,实例或组件完全成熟,可进行DOM操作。
3. beforeDestroy 和 destroy
人生看似很漫长,但在不经意之间就走向了她的终点。Vue实例或组件也一样,在经历了多姿多彩的绚烂时光后,它也逐渐走向了它生命的终点。这里提一下为啥先不说 beforeUpdate 和 updated 而是直接跳到 beforeDestroy 和 destroy,因为 beforeUpdate 和 updated 不是生命周期过程中必须执行的钩子函数。beforeUpdate 和 updated 是基于组件内数据发生变化时触发执行,如果当期实例或组件内数据只是进行显示,不进行任何修改,那么这两个钩子函数将一直不会被触发,也就不会被执行。
beforeDestroy:实例或组件销毁之前调用,在这一步,实例或组件内各参数,方法功能依然完整,实例仍完全可用。
destroy:Vue实例或组件销毁后调用。调用后,Vue 实例指示的所有东西都会自动解绑,所有的事件监听器会被移除,所有的子实例也会被销毁。
实例或组件销毁完成后,再次点击“更新内容”按钮,此时系统不再做任何响应,但,已渲染完成的Dom结构和节点元素依然存在,所以当执行完destroy操作后,实例或组件就不再受Vue系统控制。此时Vue实例或该子组件已经不存在了,实例或组件内的各参数,属性,方法均已被内存回收清空。
小结:beforeDestroy阶段,此时实例、组件虽然页面结构完整,各种功能正常,但,页面依赖参数更新后生命周期函数beforeUpdate和updated均不再执行,说明实例或组件的销毁过程一旦启动则不可逆转或中途打断。
destroy阶段,此时虽然"beforeDestroy"执行完毕,但实例指向的所有东西(参数,方法等)尚未解绑。所以此时实例内各参数、方法功能依然正常。等待"destroyed"执行完毕后,所有的东西才会被解绑,资源被回收。尘归尘,土归土,从哪来回哪去!
到此为止,Vue实例或组件从开始初始化到最终销毁,数据清空的六个钩子函数均测试完毕。这六个钩子函数是实例或组件生命周期历程中最主要的六个钩子函数,也是必须执行的六个函数,无法绕过。
4. beforeUpdate 和 updated
现在,返回来看beforeUpdate 和 updated。Vue实例或组件在挂载完成后就标志着功能健全,功能健全的组件就如成年的人生一样丰富多彩,每时每刻都可能发生变化。接下来通过修改testInfor的值,来看看beforeUpdate和updated都各自做了什么。
点击页面“更新内容”按钮,修改testInfor的值。
beforeUpdate:页面依赖的参数数据更改之后,虚拟DOM重新渲染和打补丁之前执行(此时DOM结构还未重新渲染)。
updated:页面依赖的参数数据更改之后,beforeUpdate钩子函数执行完毕,会立即进行DOM结构的重新渲染。DOM结构渲染完成之后才会调用updated钩子函数,而不是渲染时就调用。
此图就可以完全看出,调用updated时组件DOM结构已重新渲染完成,所以此时updated函数内是可以进行相关DOM操作的。
小结:在debugger beforeUpdate钩子函数时发现一个小细节,既然beforeUpdate是在页面依赖数据修改之后,虚拟DOM重新渲染之前执行,那么我在beforeUpdate函数内,是可以对依赖数据进行再次修改的,而不会导致多重渲染,也不会多次调用updated函数。
从上两图可以看出,即使在beforeUpdate函数内修改无数次页面依赖参数数据,组件Dom结构也只会重新渲染一次,即 将最后修改的依赖参数数据渲染到对应节点,updated函数也只会执行一次。只是这样做没有多大实际意义,毕竟其他地方调用其他方法更新后的数据,是页面功能需求的数据,在beforeUpdate这又瞎改一通,于功能于系统毫无益处。当然你胆肥不怕死,整一些恶搞和乱操作还是可以玩的。
虽然beforeUpdate和updated是基于页面依赖参数数据更改后触发和执行,对于页面依赖参数的变化可以起到监控作用,以及在参数变化之后执行其他后续操作,但,它们无法判定是哪个参数发生了变化。虽然每次参数数据变化之后可以通过比较各个参数值的前后值是否相等来判定是哪个参数发生了变化,但,那是基于参数量少,参数数据类型是基本数据类型的情况。一旦需要监控的参数量大,参数数据类型复杂,beforeUpdate和updated就将变得很难处理。所以实际开发过程中,除非一些特别的参数和操作,绝大部分参数的更新监听和后续操作,都是使用watch对象进行监听,因而在实际开发过程中beforeUpdate和updated使用得相当少。
另外Vue是数据驱动页面刷新,所以必然是在数据更新之后系统才会驱动虚拟DOM View层的刷新,因而beforeUpdate必然是在参数数据更新之后,View(视图)层数据(节点内数据)更新之前触发。
三、总结
总体而言,生命周期函数虽然有这么多个,但实际开发过程中使用最频繁的也就那么几个,如:created,mounted,beforeDestory,destoryed。开发人员可以:
在created内进行:页面是否加载 权限判定或页面依赖参数初始化(如按钮权限配置)、ajax数据请求、自执行函数调用等操作。
在mounted内进行:数据过滤、数据渲染赋值(如下拉框选项赋值)、DOM节点操作等功能。
在beforeDestroy内进行:参数判定,确定当前页面是否允许切换或刷新、必要数据缓存,操作记录上传等操作。
在destoryed内进行:清除当前页面其他缓存数据,如sessionStorage、定时器等。
而其他生命周期函数,并不是说它们就不重要,只是它们在平常的开发过程中,使用得不是那么频繁而已。它们也有它们自己独特的用处和用法,所以对于生命周期和生命周期函数的善加利用,可以让实际开发事半功倍,并收到良好的效果。