一文读懂Vue生命周期(Vue2)
目录
- 一文读懂Vue生命周期(Vue2)
- 1 前言
- 2 Vue生命周期
- 2.1 基本生命周期
- 2.1.1 8个生命周期
- 2.1.2 案例
- 2.2 组件生命周期
- 2.2.1 父子生命周期
- 2.2.2 案例
- 2.3 keep-alive生命周期
- 2.3.1 案例
- 2.4 其他
- 3 总结
vue生命周期详解,vue更新生命周期,keepalive生命周期,
1 前言
对于一个Vue开发者而言,学习Vue生命周期是必不可少的内容,事实上学习Vue生命周期对于开发高质量的Vue应用至关重要。在初级阶段,需要学习基本组件的生命周期,以了解页面的加载逻辑。而在Vue工程化阶段,就必须要对Vue生命周期有更深的了解。这对以下几个方面来说是基础:
- 理解组件行为:通过了解Vue的生命周期,你可以更深入地理解Vue组件是如何创建、更新和销毁的。这有助于你预测组件在不同阶段的行为,并避免潜在的问题。
- 优化性能:在生命周期的不同阶段,你可以执行特定的操作来优化应用的性能。例如,在
created
阶段,你可以进行数据的初始化或发送网络请求,以避免在模板渲染时产生不必要的延迟。在beforeDestroy
阶段,你可以清除定时器、解绑全局事件或销毁子组件,以防止内存泄漏。 - 更好的状态管理:Vue的生命周期允许你在组件的不同状态下执行特定的逻辑。例如,在
updated
阶段,你可以检查数据的变化并据此更新DOM或触发其他操作。这有助于你更好地管理组件的状态和响应数据的变化。
2 Vue生命周期
学习Vue生命周期是从简单到复杂的循序渐进过程,以下我们把其拆解成三个部分:
- 基本生命周期:单个Vue实例的生命周期;
- 组件生命周期:添加子组件时加载的生命周期;
- keep-alive生命周期:当使用keep-alive包裹组件时,该组件的生命周期;
2.1 基本生命周期
2.1.1 8个生命周期
Vue2基本生命周期钩子函数有11个,主要的生命周期有8个;主要的8个生命周期如下:
- beforeCreate(创建前):在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
- created(创建后):在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算,以及 watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。
- beforeMount(挂载前):在挂载开始之前被调用:相关的 render 函数首次被调用。此时模板编译已经完成但是还未挂载到页面上。
- mounted(挂载后):el 被新创建的 vm.el替换,并挂载到实例上去之后调用该钩子。如果root实例挂载了一个in−document元素,当mounted被调用时vm.el 也在文档内。此时可以访问到 $el 属性,也可以操作 DOM 和通过 AJAX 获取数据。
- beforeUpdate(更新前):数据更新时调用,发生在虚拟DOM打补丁之前。可以在这个钩子中访问现有的DOM,比如手动移除已添加的事件监听器。
- updated(更新后):由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件DOM已经更新,所以你现在可以执行依赖于DOM的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。
- beforeDestroy(销毁前):实例销毁之前调用。在这一步中,实例仍然完全可用。
- destroyed(销毁后):Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑,所有的事件监听器都会被移除,所有的子实例也会被销毁。
官网对8个生命周期图解如下:
2.1.2 案例
仅仅学习理论是不够的,我们来通过案例来看各个生命周期实际执行顺序和各个生命周期能访问得到的内容:
执行顺序:
直接上最简单的能触发所有生命周期代码:
<template>
<div>
<title ref="myPage">生命周期</title>
<div>参数:{{ parama }}</div>
<el-button @click="updateParama">更新数据</el-button>
<el-button @click="leavePage">离开页面</el-button>
</div>
</template>
<script>
import Vue from "vue";
import { Button } from "element-ui";
Vue.use(Button);
export default {
beforeCreate() {
console.log("页面加载:beforeCreate");
},
created() {
console.log("页面加载:created");
},
beforeMount() {
console.log("页面加载:beforeMount");
},
mounted() {
console.log("页面加载:mounted");
},
beforeUpdate() {
console.log("页面更新:beforeUpdate");
},
updated() {
console.log("页面更新:updated");
},
beforeDestroy() {
console.log("页面销毁:beforeDestroy");
},
destroyed() {
console.log("页面销毁:destroyed");
},
data() {
return {
parama: 0
};
},
methods: {
leavePage() {
this.$router.push({ path: "/home" });
},
updateParama() {
this.parama++;
}
}
};
</script>
页面效果如下:
当进入该页面时,触发前四个生命周期,执行顺序如控制台打印:
页面加载:beforeCreate
页面加载:created
页面加载:beforeMount
页面加载:mounted
当点击"更新数据"按钮时,依次触发beforeUpdate,updated;控制台打印:
页面更新:beforeUpdate
页面更新:updated
当点击“离开页面”按钮时,依次触发beforeDestroy,destroyed;控制台打印:
页面销毁:beforeDestroy
页面销毁:destroyed
大家可以复制代码自己试一下;
访问数据
再来看各个生命周期页面数据的访问情况,我们在每个生命周期都打印一下数据,代码如下:
beforeCreate() {
console.log("页面加载:beforeCreate");
console.log(this.parama);
},
created() {
console.log("页面加载:created");
console.log(this.parama);
},
beforeMount() {
console.log("页面加载:beforeMount");
console.log(this.parama);
},
mounted() {
console.log("页面加载:mounted");
console.log(this.parama);
},
beforeUpdate() {
console.log("页面更新:beforeUpdate");
console.log(this.parama);
},
updated() {
console.log("页面更新:updated");
console.log(this.parama);
},
beforeDestroy() {
console.log("页面销毁:beforeDestroy");
console.log(this.parama);
},
destroyed() {
console.log("页面销毁:destroyed");
console.log(this.parama);
},
首先是页面加载时,控制台打印如下:
页面加载:beforeCreate
parama undefined
页面加载:created
parama 0
页面加载:beforeMount
parama 0
页面加载:mounted
parama 0
我们可以看到beforeCreate
时是访问不到数据的,因为属性还未初始化;其他生命周期都能正常访问到数据。
我们再来看一下数据更新,点击“更新数据按钮”,控制台打印如下:
页面更新:beforeUpdate
parama 1
页面更新:updated
parama 1
我们可以看到两个更新的生命周期打印的数据相同,这里要注意的是,数据更新与生命周期触发的顺序是:
parama变成1 -> beforeUpdate -> updated
到这里大家都能明白生命周期顺序与数据访问之间的关系了。
2.2 组件生命周期
2.2.1 父子生命周期
当页面没有引入子组件时,如果没有做过异步处理,页面生命周期将会顺序执行,当引入子组件之后,父组件生命周期+子组件生命周期会交叉执行,具体执行顺序如下:
- 页面加载:父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
- 数据更新:当父子不涉及传参时,父子数据更新单独触发,不会相互影响;当父子涉及传参时:父beforeUpdate ->子beforeUpdate ->子updated->父updated
- 组件销毁:父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
2.2.2 案例
我们看一下案例的完整代码,将上面的案例添加子组件代码,并将父组件的参数传给子组件,子组件参数传给父组件,页面如下:
父页面:
<template>
<div>
<title ref="myPage">生命周期</title>
<div>父页面:</div>
<div>参数:{{ parama }}</div>
<div>子参数:{{ childData }}</div>
<el-button @click="updateParama">更新父数据</el-button>
<el-button @click="leavePage">离开页面</el-button>
<child :param="parama" @childData="getChildData"></child>
</div>
</template>
<script>
import Vue from "vue";
import { Button } from "element-ui";
import child from "./conponents/child";
Vue.use(Button);
export default {
beforeCreate() {
console.log("页面加载:beforeCreate");
console.log("parama", this.parama);
},
created() {
// await setTimeout({}, 1000);
console.log("页面加载:created");
console.log("parama", this.parama);
},
beforeMount() {
console.log("页面加载:beforeMount");
console.log("parama", this.parama);
},
mounted() {
console.log("页面加载:mounted");
console.log("parama", this.parama);
},
beforeUpdate() {
console.log("页面更新:beforeUpdate");
console.log("parama", this.parama);
},
updated() {
console.log("页面更新:updated");
console.log("parama", this.parama);
},
beforeDestroy() {
console.log("页面销毁:beforeDestroy");
console.log("parama", this.parama);
},
destroyed() {
console.log("页面销毁:destroyed");
console.log("parama", this.parama);
},
components: {
child
},
data() {
return {
parama: 0,
childData: 0
};
},
methods: {
leavePage() {
this.$router.push({ path: "/home" });
},
updateParama() {
this.parama++;
},
getChildData(a) {
this.childData = a;
}
}
};
</script>
子组件:
<template>
<div class="child">
<title>子组件</title>
<div>子组件:</div>
<div>参数:{{ paramaChild }}</div>
<div>父传参:{{ param }}</div>
<el-button @click="updateParama">更新子页面数据</el-button>
</div>
</template>
<script>
import Vue from "vue";
import { Button } from "element-ui";
Vue.use(Button);
export default {
beforeCreate() {
console.log("子页面加载:beforeCreate");
console.log("paramaChild", this.paramaChild);
},
created() {
// await setTimeout({}, 1000);
console.log("子页面加载:created");
console.log("paramaChild", this.paramaChild);
},
beforeMount() {
console.log("子页面加载:beforeMount");
console.log("paramaChild", this.paramaChild);
},
mounted() {
console.log("子页面加载:mounted");
console.log("paramaChild", this.paramaChild);
},
beforeUpdate() {
console.log("子页面更新:beforeUpdate");
console.log("paramaChild", this.paramaChild);
},
updated() {
console.log("子页面更新:updated");
console.log("paramaChild", this.paramaChild);
},
beforeDestroy() {
console.log("子页面销毁:beforeDestroy");
console.log("paramaChild", this.paramaChild);
},
destroyed() {
console.log("子页面销毁:destroyed");
console.log("paramaChild", this.paramaChild);
},
props: {
param: Number
},
data() {
return {
paramaChild: 0
};
},
methods: {
updateParama() {
this.paramaChild++;
this.$emit("childData", this.paramaChild);
}
}
};
</script>
<style>
.child {
margin: 20px;
border: 1px solid gray;
}
</style>
- 页面加载
页面加载时,父子生命周期:父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
控制台日志如下:
页面加载:beforeCreate
parama undefined
页面加载:created
parama 0
页面加载:beforeMount
parama 0
子页面加载:beforeCreate
undefined
子页面加载:created
paramaChild 0
子页面加载:beforeMount
paramaChild 0
子页面加载:mounted
paramaChild 0
页面加载:mounted
parama 0
- 页面更新
页面更新的情况分两类;
一类是父子组件不涉及传参,这种情况下,父页面参数更新不会触发子组件更新的生命周期,子组件的数据更新也不会触发父页面的生命周期。
第二类是父子组件涉及传参,不管是父传子还是子传父,父子生命周期相同,都为:父beforeUpdate ->子beforeUpdate ->子updated->父updated。
在以上父子相互传参案例下,我们点击父页面按钮:更新父数据;以及点击子组件按钮:更新子页面数据;控制台打印顺序都如下:
页面更新:beforeUpdate
子页面更新:beforeUpdate
子页面更新:updated
页面更新:updated
- 页面销毁
页面销毁
点击”离开页面“按钮:控制台打印顺序如下:
页面销毁:beforeDestroy
子页面销毁:beforeDestroy
子页面销毁:destroyed
页面销毁:destroyed
2.3 keep-alive生命周期
<KeepAlive>
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。被缓存的实例普通Vue实例相比,多了activated
和deactivated
两个生命周期。
- activated:被 keep-alive 缓存的组件激活时调用。
- deactivated:被 keep-alive 缓存的组件失活时调用。
光这两个生命周期来讲,就是组件激活和失活的时候调用,但直到这个是不够的,这两个生命周期与其他八个生命周期顺序是什么样子的呢,我们先说结论:(子组件被keep-alive包裹)
- 页面加载:父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->子activated(当子组件初次加载即可见时)->父mounted
- 数据更新导致组件失活:父beforeUpdate ->子deactivated->父updated
- 父页面销毁:父beforeDestroy->子deactivated(如果销毁前子组件是激活状态)->子beforeDestroy->子destroyed->父destroyed
我们结合案例来看实际情况;
2.3.1 案例
我们在案列中添加了一个组件来用于子组件的切换,并在子组件中添加activated
和deactivated
两个生命周期的监听函数,添加的代码如下:
父页面:
<template>
<div>
<title ref="myPage">生命周期</title>
<div>父页面:</div>
<div>参数:{{ parama }}</div>
<div>子参数:{{ childData }}</div>
<el-button @click="changeActiveCom">更换组件</el-button>
<el-button @click="leavePage">离开页面</el-button>
<keep-alive>
<component :is="activeComponent"></component>
</keep-alive>
</div>
</template>
<script>
import Vue from "vue";
import { Button } from "element-ui";
import child from "./conponents/child";
import comA from "./conponents/comA";
Vue.use(Button);
export default {
beforeCreate() {
console.log("页面加载:beforeCreate");
console.log("parama", this.parama);
},
created() {
// await setTimeout({}, 1000);
console.log("页面加载:created");
console.log("parama", this.parama);
},
beforeMount() {
console.log("页面加载:beforeMount");
console.log("parama", this.parama);
},
mounted() {
console.log("页面加载:mounted");
console.log("parama", this.parama);
},
beforeUpdate() {
console.log("页面更新:beforeUpdate");
console.log("parama", this.parama);
},
updated() {
console.log("页面更新:updated");
console.log("parama", this.parama);
},
beforeDestroy() {
console.log("页面销毁:beforeDestroy");
console.log("parama", this.parama);
},
destroyed() {
console.log("页面销毁:destroyed");
console.log("parama", this.parama);
},
components: {
child,
comA
},
data() {
return {
parama: 0,
childData: 0,
activeComponent: "child"
};
},
methods: {
leavePage() {
this.$router.push({ path: "/home" });
},
updateParama() {
this.parama++;
},
getChildData(a) {
this.childData = a;
},
changeActiveCom() {
this.activeComponent = this.activeComponent == "comA" ? "child" : "comA";
}
}
};
</script>
子组件child:
activated() {
console.log("child组件激活:activated");
},
deactivated() {
console.log("child组件失活:deactivated");
},
页面效果:
点击**更换组件
**按钮实现组件切换功能,用于触发activated
和deactivated
两个生命周期,
- 页面加载
页面加载时打印结果如下:
页面加载:beforeCreate
页面加载:created
页面加载:beforeMount
子页面加载:beforeCreate
子页面加载:created
子页面加载:beforeMount
子页面加载:mounted
child组件激活:activated
页面加载:mounted
- 页面更新
点击更换组件
按钮其实就进行了data数据更新操作,如果组件进入不可见状态,此时控制台打印日志如下:
页面更新:beforeUpdate
child组件失活:deactivated
页面更新:updated
如果组件进入可见状态,控制台打印如下:
页面更新:beforeUpdate
child组件失活:activated
页面更新:updated
- 页面销毁
点击离开页面
按钮,父页面销毁,此时子组件页销毁,若是此时子组件处于激活状态,那么控制台打印如下:
页面销毁:beforeDestroy
child组件失活:deactivated
子页面销毁:beforeDestroy
子页面销毁:destroyed
页面销毁:destroyed
大家可以进行案例尝试;
2.4 其他
生命周期不是一成不变的,当生命周期执行异步操作时,生命周期的结束时间顺序就可能发生变化;如下我们在父组件created
执行异步操作:
async created() {
await setTimeout({}, 1000);
console.log("页面加载:created");
},
此时控制台打印日志如下
页面加载:beforeCreate
页面加载:beforeMount
页面加载:mounted
页面加载:created
我们可以看到,created
是最后执行完的;由此,要注意异步操作的生命周期变化。
若是我们在相应生命周期做了异步操作,并返回相应的值,在后续生命周期可能会出现无法渠道返回值的情况,这也是实际生产开发容易出问题的点。特别是生产情况客户手机网络较慢的情况下。
3 总结
掌握 Vue.js 的生命周期非常重要,因为它允许你在组件的不同阶段执行自定义的代码,从而实现对组件行为的精细控制和管理。但同时也有一些注意事项,以下也是一些注意事项:
- 避免在
render
函数中直接修改数据: 在render
函数中修改数据可能会导致不可预料的结果,因为render
函数是用来生成虚拟 DOM 的,应该是一个纯函数。如果需要在渲染过程中修改数据,应该使用computed
属性或者watch
监听器。 - 谨慎使用异步操作: 在生命周期钩子函数中进行异步操作时,一定要注意异步操作完成的时机和影响。尤其是在
created
钩子中,当组件实例已经创建但是 DOM 还未挂载时,执行异步操作可能会导致一些问题。最好的做法是在mounted
钩子中执行异步操作。 - 避免频繁使用
beforeUpdate
和updated
: 虽然beforeUpdate
和updated
钩子提供了在组件更新时执行逻辑的机会,但频繁使用这些钩子可能会导致性能问题。应该仔细考虑哪些逻辑需要在组件更新时执行,以避免不必要的性能开销。 - 合理利用
activated
和deactivated
钩子: 当使用<keep-alive>
组件缓存组件时,activated
和deactivated
钩子会被调用。在这些钩子中,你可以执行一些与组件缓存和恢复相关的逻辑,比如重置组件状态或重新加载数据。 - 在
beforeDestroy
进行清理: 在beforeDestroy
钩子中,你可以执行一些清理工作,比如清除定时器、取消订阅、解绑事件等。这样可以确保在组件销毁前进行必要的资源释放,避免内存泄漏和其他问题。