超详细vue生命周期解析(详解)

vue是每一个前端开发人员都绕不过的一个技术,在国内的市场占有量也是非常的大,我们大部分人用着vue, 却不知道他内部其实经历了一些什么。每个生命周期又是什么时候开始执行的。我们今天来详细的看一看

首先,生命周期是个啥?
借用官网的一句话就是:每一个vue实例从创建到销毁的过程,就是这个vue实例的生命周期。在这个过程中,他经历了从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、卸载等一系列过程。那么这些过程中,具体vue做了些啥,我们今天来了解一下。

语述

了解之前,我们先贴上一张官网的生命周期图,从图上,我们再一步一步来理解vue生命周期。
在这里插入图片描述
我们先简单的来解说这张图,然后再通过例子来详看
首先,从图上,我们可以看出,他的一个过程是

  1. new Vue()实例化一个vue实例,然后init初始化event 和 lifecycle, 其实这个过程中分别调用了3个初始化函数(initLifecycle(), initEvents(), initRender()),分别初始化了生命周期,事件以及定义createElement函数,初始化生命周期时,定义了一些属性,比如表示当前状态生命周期状态得_isMounted ,_isDestroyed ,_isBeingDestroyed,表示keep-alive中组件状态的_inactive,而初始化event时,实际上就是定义了$once、$off、$emit、$on几个函数。而createElement函数是在初始化render时定义的(调用了initRender函数)
  2. 执行beforeCreate生命周期函数
  3. beforeCreate执行完后,会开始进行数据初始化,这个过程,会定义data数据,方法以及事件,并且完成数据劫持observe以及给组件实例配置watcher观察者实例。这样,后续当数据发生变化时,才能感知到数据的变化并完成页面的渲染
  4. 执行created生命周期函数,所以,当这个函数执行的时候,我们已经可以拿到data下的数据以及methods下的方法了,所以在这里,我们可以开始调用方法进行数据请求了
  5. created执行完后,我们可以看到,这里有个判断,判断当前是否有el参数(这里为什么需要判断,是因为我们后面的操作是会依赖这个el的,后面会详细说),如果有,我们再看是否有template参数。如果没有el,那么我们会等待调用$mount(el)方法(后面会详细说)。
  6. 确保有了el后,继续往下走,判断当有template参数时,我们会选择去将template模板转换成render函数(其实在这前面是还有一个判断的,判断当前是否有render函数,如果有的话,则会直接去渲染当前的render函数,如果没有那么我们才开始去查找是否有template模板),如果没有template,那么我们就会直接将获取到的el(也就是我们常见的#app,#app里面可能还会有其他标签)编译成templae, 然后在将这个template转换成render函数。
  7. 之后再调用beforMount, 也就是说实际从creted到beforeMount之间,最主要的工作就是将模板或者el转换为render函数。并且我们可以看出一点,就是你不管是用el,还是用template, 或者是用我们最常用的.vue文件(如果是.vue文件,他其实是会先编译成为template),最终他都是会被转换为render函数的。
  8. beforeMount调用后,我们是不是要开始渲染render函数了,首先我们会先生产一个虚拟dom(用于后续数据发生变化时,新老虚拟dom对比计算),进行保存,然后再开始将render渲染成为真实的dom。渲染成真实dom后,会将渲染出来的真实dom替换掉原来的vm.$el(这一步我们可能不理解,请耐心往下看,后面我会举例说明),然后再将替换后的$el append到我们的页面内。整个初步流程就算是走完了
  9. 之后再调用mounted,并将标识生命周期的一个属性_isMounted 置为true。所以mounted函数内,我们是可以操作dom的,因为这个时候dom已经渲染完成了。
  10. 再之后,只有当我们状态数据发生变化时,我们在触发beforeUpdate,要开始将我们变化后的数据渲染到页面上了(实际上这里是有个判断的,判断当前的_isMounted是不是为ture并且_isDestroyed是不是为false,也就是说,保证dom已经被挂载的情况下,且当前组件并未被销毁,才会走update流程)
  11. beforeUpdate调用之后,我们又会重新生成一个新的虚拟dom(Vnode),然后会拿这个最新的Vnode和原来的Vnode去做一个diff算,这里就涉及到一系列的计算,算出最小的更新范围,从而更新render函数中的最新数据,再将更新后的render函数渲染成真实dom。也就完成了我们的数据更新
  12. 然后再执行updated,所以updated里面也可以操作dom,并拿到最新更新后的dom。不过这里我要插一句话了,mouted和updated的执行,并不会等待所有子组件都被挂载完成后再执行,所以如果你希望所有视图都更新完毕后再做些什么事情,那么你最好在mouted或者updated中加一个$nextTick(),然后把要做的事情放在$netTick()中去做(至于为什么,以后讲到$nextTick再说吧)
  13. 再之后beforeDestroy没啥说的,实例销毁前,也就是说在这个函数内,你还是可以操作实例的
  14. 之后会做一系列的销毁动作,解除各种数据引用,移除事件监听,删除组件_watcher,删除子实例,删除自身self等。同时将实例属性_isDestroyed置为true
  15. 销毁完成后,再执行destroyed

示例

大致过程就是这样,下面我们来通过例子来看一看

<body>
    <div id="app">
        <p>{{message}}</p>
        <button @click="changeMsg">改变</button>
    </div>
</body>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            message: 'hello world'
        },
        methods: {
            changeMsg () {
                this.message = 'goodbye world'
            }
        },
        beforeCreate() {
            console.log('------初始化前------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成---------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeUpdate () {
            console.log('------更新前---------')
            console.log(this.message)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后---------')
            console.log(this.message)
            console.log(this.$el)
        }
    })
</script>

我们先看看首次加载时,输出了啥
在这里插入图片描述

从上面我们可以看出几点,

  • 首次,只执行了4个生命周期,beforeCreate,created, beforeMount, mounted。
  • 同时,我们可以看出,第一个生命周期中,我们拿不到data中的数据,因为这个时候数据还未初始化
  • created中,我们可以拿到data中的message数据了,因为初始化已经完成
  • beforeMount中,我们可以看出,我们拿到了$el,而mounted中,我们也拿到了$el, 不过好像有点不一样是吧。一个好像是渲染前的,一个是渲染后的。对的。看过MVVM响应式原来或者Vue源码你们就会发现,最初其实我们是会去让this.$el = new Vue时传入的那个el的dom。所以在beforMount中,其实我们拿到的就是页面中的#app。而再继续往后,首先我们是不是没有找到render函数啊,也没有找到template啊,所以他会怎么做啊,是不是会把我们的这个el(#app)编译成template模板啊,再转换为render函数,最后将render函数渲染成为真实dom,渲染成真实dom后,我们是不是会用这个渲染出来的dom去替换原来的vm.$el啊。这也就是我们前面所说到的替换$el是什么意思了。
  • 所以, 在mounted中,我们所得到的渲染完成后的$el。

下面我们再看个例子

var vm = new Vue({
        el: '#app',
        data: {
            message: 'hello world'
        },
        template: '<div>我是模板内的{{message}}</div>',
        methods: {
            changeMsg () {
                this.message = 'goodbye world'
            }
        },
        beforeCreate() {
            console.log('------初始化前------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成---------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeUpdate () {
            console.log('------更新前---------')
            console.log(this.message)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后---------')
            console.log(this.message)
            console.log(this.$el)
        }
    })

我们在new Vue实例的时候直接传入了一个template,这时候我们再看输出
在这里插入图片描述
这么看是不是就很清晰了啊 ,在beforeMount的时候,$el还是#app, 但是在mounted的时候就变成模板的div了,是不是因为我们传了个template啊,所以,他直接将这个template转换成render函数啦。再渲染成真实dom后,用渲染出来的真实dom替换了原来的$el。

下面我们删除上面的template, 点击按钮更改下message,查看输出
在这里插入图片描述
哎。。。有没有看到一个很奇怪的东西啊,在beforeUpdate中输出的$el居然和updated里面输出的是一样的。这不对啊,以我们上面所说的逻辑的话,beforeUpdate内的$el应该是更新前的啊。这是怎么回事呢。这时候我们先来看一下mounted里面的。mounted里面我们看到p标签内依旧是hello world 对不对,其实这是因为,我是先点击了#app那个div的箭头,将这个div展开了以后,我再点击的按钮去更改了message,所以mounted里面还是原来的。那我现在如果先不展开mounted里面的div的话,我们来看看会怎么样

在这里插入图片描述

可以看到,初始输出,其实是这样的,我们看不到#app内的东西,需要点击箭头展开才能看到,现在,我不展开,然后我先点击按钮去改变message, 等beforUpdate和updated都执行完成后,我们再来一起展开,看下会怎么样
在这里插入图片描述
这是点击改变了message后的截图,然后我们现在展开div看看
在这里插入图片描述
看到没有,我们发现什么啦,怎么现在mounted里面的$el也变成更新后的啦。
呵呵,不要慌,其实啊,因为this.$el是一个对象,其实本质就是一个指针,当我们刚console.log输出的时候,其实并没有显示内容,而当我们点击箭头去展开这个div的时候,将指针指向了当前的$el,所以我们看到的才会都是改变后的$el。这也就是为什么之前mounted里面的$el是改变之前的值,而现在是改变之后的值了,因为之前那张图,我是先展开了mounted中的div,再去改变的message。下面我们再来验证下是不是这么回事
怎么验证,我们修改下代码

mounted () {
            console.log('------挂载完成---------')
            console.log(this.message)
            console.log(this.$el.innerHTML)
            console.log(this.$el)
        },
        beforeUpdate () {
            console.log('------更新前---------')
            console.log(this.message)
            console.log(this.$el.innerHTML)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后---------')
            console.log(this.message)
            console.log(this.$el.innerHTML)
            console.log(this.$el)
        }

我们增加一个输出 this.$el.innerHTML, 再查看结果
在这里插入图片描述

这么看是不是就很明了啦,beforeUpdate里面的$el的内容,确实还是改变之前的,而我们之前看到的,只是因为我们后面展开时指针指向了当前值才导致的,是个视觉差而已。

后面两个销毁的,我就不举例说明了,没啥说的。下面我们再看一个问题,就是如果我们没有设置el时,会怎么样,我们在之前的生命周期图中,是说过,当没有找到el时, 说是不是会等待vm.$mount(el) 啊,这句话啥意思,我们来看一下
在这里插入图片描述
首先,我们看下,vue源码中,
在这里插入图片描述
在执行完,beforeCreate和created之后,是做了个判断,当存在el时,调用了 $mount方法,created之后的步骤,就是在这里面去走的。那如果没有el呢, 生命周期图中是说等待vm. $mount调用。那是不是只能等待我们手动去调用啊。

var vm = new Vue({
        data: {
            message: 'hello world'
        },
        // template: '<div>我是模板内的{{message}}  <button @click="changeMsg">点我</button></div>',
        methods: {
            changeMsg () {
                this.message = 'goodbye world'
            }
        },
        beforeCreate() {
            console.log('------初始化前------')
            console.log(this.message)
            console.log(this.$el)
        },
        created () {
            console.log('------初始化完成------')
            console.log(this.message)
            console.log(this.$el)
        },
        beforeMount () {
            console.log('------挂载前---------')
            console.log(this.message)
            console.log(this.$el)
        },
        mounted () {
            console.log('------挂载完成---------')
            console.log(this.message)
            console.log(this.$el.innerHTML)
            console.log(this.$el)
        },
        beforeUpdate () {
            console.log('------更新前---------')
            console.log(this.message)
            console.log(this.$el.innerHTML)
            console.log(this.$el)
        },
        updated() {
            console.log('------更新后---------')
            console.log(this.message)
            console.log(this.$el.innerHTML)
            console.log(this.$el)
        }
    })

这个时候,我们删除了el属性,看看结果
在这里插入图片描述
是不是只走了前面两个生命周期啊,后面就没走了,这个时候其实就是在等$mount被调用了,那我们加个按钮,点击按钮,手动调用一下$mount看会怎样
在这里插入图片描述
没点击之前
在这里插入图片描述
点击后
在这里插入图片描述
可以看到,生命周期继续往下走了。
这时候不知道大家是不是想起来,看到有些vue项目的main.js里面是这样的

export default new Vue({
  el: '#app',
  router,
  store,
  i18n,
  render: h => h(App)
})

而有些vue项目中人家用的又是这样的

export default new Vue({
  router,
  store,
  i18n,
  render: h => h(App)
}).$mount('#app')

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

好了,言尽于此,有没有看懂的朋友,请直接私信或者评论。


文章标签:

原文连接:https://blog.csdn.net/weixin_42707287/article/details/111641286

相关推荐

Webpack学习系列 - Webpack5 怎么集成Babel ?

Webpack干货系列 | 在 Webpack 5 集成 ESLint 的方法

使用开源Cesium+Vue实现倾斜摄影三维展示

使用 Abp.Zero 搭建第三方登录模块(三):网页端开发

Vue初探

【前端面试】Vue面试题总结(持续更新中)

Re:用webpack从零开始的vue-cli搭建'生活'

vue3代码编写

Vue 路由懒加载, VueRouter一步完成Vue的路由懒加载 一行代码搞定懒加载

SpringBoot+Vue实现简单用户管理平台第二篇(前端设计,接口对接)

SpringBoot+Vue实现前后端分离的实习管理系统

Spring Boot+Vue3 动态菜单实现思路梳理

vue3 axios全局配置封装

热门App 高仿项目12 个 GitHub 上的高仿项目

vue2升级vue3指南(一)—— 环境准备和构建篇

TypeScript--如何在 vue中使用?

Vite+TS带你搭建一个属于自己的Vue3组件库

vue封装原生的可预览裁剪上传图片插件H5,PC端都可以使用

Java 之SpringBoot+Vue实现后台管理系统的开发

基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结