vue生命周期详解

884 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

一、什么是生命周期

官方:每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

通俗来说,就是 Vue 实例从创建到销毁的整个过程就是生命周期。

二、生命周期图示详解

如上图所示是 Vue 的整个生命周期过程,分析如下:

1、new Vue() 就是实例化一个 vue 实例。

2、然后进行 Init 初始化,在这个过程中分别调用了3个初始化函数(initLifecycle(),initEvents(),initRender())。

initLifecycle():初始化生命周期,定义了一些属性,以及为 parent,child 属性赋值,属性如下图所示:

参考文章:blog.csdn.net/weixin_3403…

initEvents():初始化事件队列以及监听器,定义了 onceonce、off、emitemit、on 这四个函数;

initRender():初始化 render 函数,定义了 createElement 函数。

3、执行 beforeCreate 生命周期函数,在该阶段,数据还没有挂载,无法访问到数据和DOM,所以我们一般不做操作。

4、beforeCreate 执行完后,会开始进行数据初始化,这个过程会定义 data 数据、方法以及事件,并且完成数据劫持以及给组件实例配置 watcher 观察者实例。这样,后续当数据发生变化时,才能感知到数据的变化并完成页面的渲染。

5、执行 created 生命周期函数,这个时候已经可以拿到 data 下的数据以及 methods 下的方法了,在渲染前可以在这里更改数据且不会触发其他的钩子函数(例如 updated 函数),所以一般可以在这里调用方法进行数据请求(例如初始数据的获取)。

6、created 执行完后,接下来有个判断,判断当前是否有 el 参数(后面的操作会依赖这个el)。判断如果有 el,我们再判断是否有 template 参数;如果没有 el ,那么我们会等待调用  $mount(el) 方法(请看“三、代码示例”的补充)。

7、确保有了 el 后,继续往下走,判断当有 template 参数时,我们会选择去将 template 模板转换成 render 函数(其实在这前面是还有一个判断的,判断当前是否有 render 函数,如果有的话,则会直接去渲染当前的 render 函数,如果没有那么我们才开始去查找是否有 template 模板);如果没有 template,那么我们就会直接将获取到的 el(也就是我们常见的 #app,#app 里面可能还会有其他标签)编译成 templae,然后再将这个 template 转换成 render 函数。

8、执行 beforMount 生命周期函数,也就是说实际从 creted 到 beforeMount 之间,最主要的工作就是将 template 模板或者 el 转换为 render 函数。并且我们可以看出一点,就是不管是用 el ,还是用 template ,或者是用我们最常用的 .vue 文件(如果是 .vue 文件,他其实是会先编译成为 template ),最终他都是会被转换为 render 函数。由于也是渲染前,所以在这里也可以更改数据且不会触发其他的钩子函数,同理也可以在这里做初始数据的获取,但是我们更多的还是在 created 中做初始数据的获取。

9、beforeMount 执行完后,就要开始渲染 render 函数了,首先我们会先生产一个虚拟 dom (用于后续数据发生变化时,新老虚拟 dom 对比计算),进行保存,然后再开始将 render 渲染成为真实的 dom 。渲染成真实dom后,会将渲染出来的真实 dom 替换掉原来的 vm.el,然后再将替换后的el ,然后再将替换后的 el append 到我们的页面内。整个初步流程就算是走完了。

10、执行 mounted 生命周期函数,并将标识生命周期的一个属性 _isMounted 置为 true 。此时,dom 已经渲染完成了,可以在这里操作 dom 了。

11、接下来,当状态数据发生变化时,就会触发 beforeUpdate 生命周期函数,将我们变化后的数据渲染到页面上了(前提是当前的 _isMounted 为 ture 且 _isDestroyed 为 false ,也就是要保证 dom 已经被挂载且当前组件并未被销毁,才会走 update 流程)

12、beforeUpdate 执行完后,我们又会重新生成一个新的虚拟 dom(Vnode),然后会拿这个最新的 Vnode 和原来的 Vnode 利用 diff 算法进行对比,算出最小的更新范围,从而更新  render 函数中的最新数据,再将更新后的 render 函数渲染成真实 dom 。也就完成了我们的数据更新。

13、执行 updated 生命周期函数,此时,数据已经更改完成,dom 也重新 render 完成,可以操作更新后的 dom 。注意:mouted 和 updated 的执行,不会等待所有子组件都被挂载完成后再执行,所以如果希望所有视图都更新完毕后再做些什么操作,那么最好在 mouted 或者updated中加一个 nextTick()函数,然后把要做的事情放在nextTick() 函数,然后把要做的事情放在 netTick() 中去做。

14、当 $destroy 方法被调用,就会触发 beforeDestroy 生命周期函数(表示实例销毁前),在该函数中还是可以操作实例的,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等。

15、beforeDestroy 执行完后,会做一系列的销毁动作,比如解除各种数据引用,移除事件监听,删除组件 _watcher,删除子实例,删除自身 self 等。同时将实例属性 _isDestroyed 置为 true 。

16、销毁完成后,执行 destroyed 生命周期函数。

三、代码示例

1、新建文件“生命周期.html”,代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>生命周期</title>
	<!-- 开发环境版本,包含了有帮助的命令行警告 -->
	<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
	<div id="app">
  		<button @click="changeCon">改变内容</button>
  		<p class="content">{{ message }}</p>
	</div>
</body>
<script type="text/javascript">
	var app = new Vue({
  		el: '#app',
  		data: {
    		message: 'Hello Vue!'
 		 },
 		 methods: {
            changeCon () {
                this.message = 'Hello world!'
            }
        },
        beforeCreate() {
            console.log('------初始化前beforeCreate------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成created------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前beforeMount---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成mounted---------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeUpdate () {
            console.log('------更新前beforeUpdate---------')
            console.log(this.message)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后updated---------')
            console.log(this.message)
            console.log(this.$el)
        }
	})
</script>
</html>

浏览器打开,首次加载,控制台输出如图所示:

从上图分析可知:

a、首次加载只执行了四个生命周期函数(beforeCreate,created,beforeMount,mounted)。

b、在 beforeCreate 生命周期函数中,拿不到 data 中的数据(数据还未初始化)和 $el。

c、在 created 生命周期函数中,可以拿到 data 中的数据(初始化已经完成)但仍然拿不到 $el 。

d、在 beforeMount 生命周期函数中,不仅拿到了 data 中的数据还拿到了 $el 。

e、在 mounted 生命周期函数中,我们也拿到了 data 中的数据和 $el 。

说明:通过对比 “d” 和 “e” 可知,虽然在这两个生命周期函数中,都拿到了 el,不过有点不一样。“d”中的el ,不过有点不一样。“d” 中的 el 是渲染前的,“e” 中的 el是渲染后的,这是对的。那是因为最初拿的是this.el 是渲染后的,这是对的。那是因为最初拿的是 this.el = new Vue 时传入的那个 el 的 dom(通过分析MVVM响应式原理或者 Vue 源码就会发现),所以在 beforMount 中,其实我们拿到的就是最初页面中的 #app。而再继续往后,首先是因为没有找到 render 函数,也没有找到 template ,所以程序会把这个 el(也就是#app)编译成 template 模板,再转换为 render 函数,最后将 render 函数渲染成为真实的 dom ,然后会用这个渲染出来的 dom 替换原来的 vm.el。这也就是前面所说到的替换el 。这也就是前面所说到的替换 el 是什么意思了。所以, 在 mounted 中,我们所得到的 el其实是被替换的,渲染完成后的el 其实是被替换的,渲染完成后的 el 。

2、对上面说明部分还不是很明白的,可以再看接下来的例子,代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>生命周期</title>
	<!-- 开发环境版本,包含了有帮助的命令行警告 -->
	<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
	<div id="app">
  		<button @click="changeCon">改变内容</button>
  		<p class="content">{{ message }}</p>
	</div>
</body>
<script type="text/javascript">
	var app = new Vue({
  		el: '#app',
  		data: {
    		message: 'Hello Vue!'
 		 },
 		 template: '<div>我是vue实例中默认模板内的{{message}}</div>',
 		 methods: {
            changeCon () {
                this.message = 'Hello world!'
            }
        },
        beforeCreate() {
            console.log('------初始化前beforeCreate------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成created------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前beforeMount---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成mounted---------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeUpdate () {
            console.log('------更新前beforeUpdate---------')
            console.log(this.message)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后updated---------')
            console.log(this.message)
            console.log(this.$el)
        }
	})
</script>
</html>

由以上代码可知,在 new Vue 实例的时候直接传入了一个 template ,再看控制台输出如图所示:

由上图可知,在 beforeMount 的时候,el 还是 #app , 但是在 mounted 的时候就变成 vue 实例中的 template 模板中的 div 了,这是因为传了这个 template 后,程序直接将这个 template 转换成了 render 函数。再渲染成真实 dom ,并用渲染出来的真实 dom 替换了原来的 el 。

3、删除上面的 template , 重新加载页面,来看点击“改变内容”按钮来更改 data 中的 message 后,控制台输出的对比,此部分分两种情况说明:

第一种情况如图所示:

由上图可知,在 beforeUpdate 中输出的 el居然和updated里面输出的是一样的。这肯定不对,按照之前所说的逻辑,beforeUpdate内的el 居然和 updated 里面输出的是一样的。这肯定不对,按照之前所说的逻辑,beforeUpdate 内的 el 应该是更新前的,不应该跟 updated 内的一样,而应该跟 mounted 内的一样为 Hello Vue! 。造成这种情况的原因其实是页面加载完成后,我先点击了控制台输出中 mounted 中 id 为 #app 的那个 div 的小箭头。将这个 div 展开后,才点击“改变内容”按钮去更改 message ,所以 mounted 里面还是原来的内容。

第二种情况:页面加载完成后,先不展开 mounted 里面的 div ,直接点击“改变内容”按钮去更改 message ,控制台输出如图所示:

再展开 div ,结果如图所示:

由上图可知, mounted 中输出的 elbeforeUpdate中输出的el 和 beforeUpdate 中输出的 el 虽然一致了,但是和 updated 中输出的 el也一样了,很显然有问题。研究后发现,出现这种情况的原因是因为this.el 也一样了,很显然有问题。研究后发现,出现这种情况的原因是因为 this.el 是一个对象,其本质就是一个指针,当我们刚 console.log 输出的时候,其实并没有显示内容,而当我们点击箭头去展开这个 div 的时候,将指针指向了当前的 el,所以我们看到的其实都是改变后的el ,所以我们看到的其实都是改变后的 el 。这也就是为什么第一种情况中 mounted 里面的 el是改变之前的值,而beforeUpdate是改变之后的值,以及第二种情况中mountedbeforeUpdate里面的el 是改变之前的值,而 beforeUpdate 是改变之后的值,以及第二种情况中 mounted 和 beforeUpdate 里面的 el 都变成改变之后的值了,这其实是一种假象。

4、以上内容不明白的,下面可以通过增加一个输出 this.$el.innerHTML 再来验证下,更改后的代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>生命周期</title>
	<!-- 开发环境版本,包含了有帮助的命令行警告 -->
	<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
	<div id="app">
  		<button @click="changeCon">改变内容</button>
  		<p class="content">{{ message }}</p>
	</div>
</body>
<script type="text/javascript">
	var app = new Vue({
  		el: '#app',
  		data: {
    		message: 'Hello Vue!'
 		 },
 		 // template: '<div>我是vue实例中默认模板内的{{message}}</div>',
 		 methods: {
            changeCon () {
                this.message = '我是由“改变内容”按钮所触发!'
            }
        },
        beforeCreate() {
            console.log('------初始化前beforeCreate------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成created------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前beforeMount---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成mounted---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        beforeUpdate () {
            console.log('------更新前beforeUpdate---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        updated() {
            console.log('------更新后updated---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        }
	})
</script>
</html>

控制台输出如图所示:

由上图可以看出,mounted 和 beforeUpdate 里面的 $el 的内容确实还是改变之前的,这也验证了之前看到的只是因为后面展开 div 时指针指向了当前值才导致的,是个视觉差而已。

5、下面增加一个“销毁”按钮来触发后面的 beforeDestroy 和 destroyed ,代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>生命周期</title>
	<!-- 开发环境版本,包含了有帮助的命令行警告 -->
	<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
	<div id="app">
  		<button @click="changeCon">改变内容</button>
        <button @click="destroy">销毁</button>
  		<p class="content">{{ message }}</p>
	</div>
</body>
<script type="text/javascript">
	var app = new Vue({
  		el: '#app',
  		data: {
    		message: 'Hello Vue!'
 		 },
 		 // template: '<div>我是vue实例中默认模板内的{{message}}</div>',
 		 methods: {
            // 改变内容
            changeCon () {
                this.message = '我是由“改变内容”按钮所触发!'
            },
            // 销毁
            destroy:function(){
                this.$destroy()//调用$destroy()方法
            }
        },
        beforeCreate() {
            console.log('------初始化前beforeCreate------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成created------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前beforeMount---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成mounted---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        beforeUpdate () {
            console.log('------更新前beforeUpdate---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        updated() {
            console.log('------更新后updated---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        beforeDestroy:function(){
            console.log('------销毁前beforeDestroy---------')
            this.message='我是销毁前改的内容'
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        destroyed:function(){
            console.log('------销毁后destroyed---------')
            this.message='我是销毁后改的内容'
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        }
	})
</script>
</html>

加载页面完成后,点击“改变内容”按钮,再点击“销毁”按钮,控制台输出如下图所示:

由上图可知,beforeDestroy 和 destroyed 中虽然改变了 data 中 message 的值,但是页面并没有渲染(还是 updated 时的值),这说明当 $destroy 方法被调用后,虽然 DOM 节点依旧存在,但 vue 实例解除了 dom 的绑定(无响应了)。

6、补充

还有一个问题,就是如果没有设置 el 时,会怎么样。在之前的生命周期图中说过“如果没有 el,那么我们会等待调用 $mount(el) 方法”,这句话具体意思现在可以来解释一下。

首先看下,vue 源码中,

在执行完 beforeCreate 和 created 之后做了个判断,当存在 el 时,调用了 mount()方法,created之后的步骤,就是在这里面去走的。如果没有el,按生命周期图中是说等待vm.mount() 方法, created 之后的步骤,就是在这里面去走的。如果没有 el ,按生命周期图中是说等待 vm. mount 调用,意思就是等待我们自己手动去调用。删除了 el 属性后的代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>生命周期</title>
	<!-- 开发环境版本,包含了有帮助的命令行警告 -->
	<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
	<div id="app">
  		<button @click="changeCon">改变内容</button>
        <button @click="destroy">销毁</button>
  		<p class="content">{{ message }}</p>
	</div>
</body>
<script type="text/javascript">
	var app = new Vue({
  		// el: '#app',//删除el
  		data: {
    		message: 'Hello Vue!'
 		 },
 		 // template: '<div>我是vue实例中默认模板内的{{message}}</div>',
 		 methods: {
            // 改变内容
            changeCon () {
                this.message = '我是由“改变内容”按钮所触发!'
            },
            // 销毁
            destroy:function(){
                this.$destroy()//调用$destroy()方法
            }
        },
        beforeCreate() {
            console.log('------初始化前beforeCreate------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成created------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前beforeMount---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成mounted---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        beforeUpdate () {
            console.log('------更新前beforeUpdate---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        updated() {
            console.log('------更新后updated---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        beforeDestroy:function(){
            console.log('------销毁前beforeDestroy---------')
            this.message='我是销毁前改的内容'
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        destroyed:function(){
            console.log('------销毁后destroyed---------')
            this.message='我是销毁后改的内容'
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        }
	})
</script>
</html>

页面加载后,控制态输出如图所示:

由上图可知,只走了前面两个生命周期啊,后面就没走了,这个时候其实就是在等 mount被调用了,那加个“调用mount”按钮,点击按钮,手动调用一下mount 被调用了,那加个“调用mount”按钮,点击按钮,手动调用一下 mount 看会怎样,代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>生命周期</title>
	<!-- 开发环境版本,包含了有帮助的命令行警告 -->
	<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
	<div id="app">
  		<button @click="changeCon">改变内容</button>
        <button @click="destroy">销毁</button>
        <!-- <button id="mountBtn" @click="mountBtn">调用mount</button> -->
        <button id="mountBtn">调用mount</button>
  		<p class="content">{{ message }}</p>
	</div>
</body>
<script type="text/javascript">
    var dom = document.getElementById('mountBtn')
    dom.onclick = function(){
        app.$mount("#app")
    }
	var app = new Vue({
  		// el: '#app',//el
  		data: {
    		message: 'Hello Vue!'
 		 },
 		 // template: '<div>我是vue实例中默认模板内的{{message}}</div>',
 		 methods: {
            // 改变内容
            changeCon () {
                this.message = '我是由“改变内容”按钮所触发!'
            },
            // 销毁
            destroy:function(){
                this.$destroy()//调用$destroy()方法
            },
            // 调用mount
            // mountBtn:function(){
            //     app.$mount("#app")
            // }
        },
        beforeCreate() {
            console.log('------初始化前beforeCreate------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成created------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前beforeMount---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成mounted---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        beforeUpdate () {
            console.log('------更新前beforeUpdate---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        updated() {
            console.log('------更新后updated---------')
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        beforeDestroy:function(){
            console.log('------销毁前beforeDestroy---------')
            this.message='我是销毁前改的内容'
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        },
        destroyed:function(){
            console.log('------销毁后destroyed---------')
            this.message='我是销毁后改的内容'
            console.log(this.message)
            console.log(this.$el)
            console.log(this.$el.innerHTML)
        }
	})
</script>
</html>

注意:此处有坑,不能将调用 mount 的方法直接写在 methods 中且实例名称必须保持一致,如图所示:

没点击“调用mount”按钮之前,如图所示:

点击“调用mount”按钮后,如图所示:

由上图可知,生命周期继续往下走了。

这就是为什么,有时候我们看到有些 vue 项目中的 main.js 里面是这样的:

而有些 vue 项目中又是这样的:

其实后者,就相当于是手动调用了 $mount 了。

以上就是整个 vue 的生命周期过程了。

原文链接:blog.csdn.net/weixin_4270…