irpas技术客

超详细vue生命周期解析(详解)_ら陈佚晨_vue生命周期

网络 5496

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

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

语述

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

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

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


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #vue生命周期