vue进阶测试——生命周期和异步加载的微妙关系

    本文继续探索vue中的坑,关于vue的生命周期和异步加载相关处理的文章还比较少,可能是自己没有用谷歌而用百度的缘故吧。关于异步操作和生命周期,会牵扯到浏览器的单线程处理机制,以及ES中的promise对象,有兴趣的可以事先了解一下再看下去,当然我自己对单线程的处理机制可能了解的也不是很深,写的不对,请指正。

    关于本文中生命周期的理解,一切以实际出发,为什么要强调这个,因为实际情况和官方API有些出入,当然我会说明发生这种情况的两种合理的解释。

    首先,要明确一件事,我在做vue开发的时候,用的是vue-cli脚手架工具搭建的环境,如果是初学者,请务必!!!不要往下看了,因为下面的内容会让你对vue的生命周期造成误解。

如果你对生命周期的概念还比较模糊,请顺序阅读

    vue的生命周期一共有 6+2 个阶段

    分别是:(6)

    创建前(beforeCreate),没数据,没dom

    创建完毕(created),有数据,没dom

    挂载dom前(beforemount),说实话,如果你真的是新手的话,挂载两个字可能不是很理解,因为我之前也觉得说这么深奥搞鸡毛,我根本听不懂好吧,当然我现在觉得这不是说的很对吗!那我通俗的解释一下,挂载前,就是vue生成了一堆虚拟的dom,你可以理解为一个json格式的字符串。如下所示,生成的虚拟dom还没有解析双大括号里面的内容,只是单纯的放着。

“father:
 爸爸的节点:div
 爸爸的class:'balabla'
 爸爸的儿子:
   儿子的节点:'span'
    ............”  

    挂载dom完毕(mounted),根据上面的说法,这一步骤就好理解了,就是说dom中的各种vue特性如双大括号绑定数据之类的都搞定了,而且我把它放到html文档流中去了。

    销毁前,

    销毁完毕 ,关于销毁的触发,你可以尝试用v-if,这是坠简单的,同时你也能发现v-if和v-show的本质区别了,当然在单页面应用中(vue-router),你也可以用“跳转页面”实现上一页面的组件销毁,了解一下就行。

    (2)

    更新视图前,

    更新视图完毕,这两个周期都在更新数据时触发,是数据驱动模版最重要的一环。

    关于这八个生命周期,先聊一下个人的理解,其实我觉得他的设计初衷跟人的生老病死是一样的,前六个对应的分别是

    出生前,出生,学校阶段,学以致用阶段,准备后事,入土为安

    至于触发更新,就不在这六个阶段之内了

    那么,这六个阶段是不是按顺序执行的呢?可以说是,也可以说不是。

    先说为什么是,来看我网上抄来的一段代码

<template>
  <div class="">
    <div>{{ message }}</div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      message: 'hello world',
    }
  },
  beforeCreate: function () {
    console.group('beforeCreate 创建前状态===============》');
    console.log("%c%s", "color:red" , "el     : " + this.$el); //undefined
    console.log("%c%s", "color:red","data   : " + this.$data); //undefined 
    console.log("%c%s", "color:red","message: " + this.message)  
  },
  created: function () {
    console.group('created 创建完毕状态===============》');
    console.log("%c%s", "color:red","el     : " + this.$el); //undefined
    console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化 
    console.log("%c%s", "color:red","message: " + this.message); //已被初始化
  },
  beforeMount: function () {
    console.group('beforeMount 挂载前状态===============》');
    console.log("%c%s", "color:red","el     : " + (this.$el)); //已被初始化
    console.log(this.$el);
    console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化  
    console.log("%c%s", "color:red","message: " + this.message); //已被初始化  
  },
  mounted: function () {
    console.group('mounted 挂载结束状态===============》');
    console.log("%c%s", "color:red","el     : " + this.$el); //已被初始化
    console.log(this.$el);    
    console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化
    console.log("%c%s", "color:red","message: " + this.message); //已被初始化 
  },
  beforeUpdate: function () {
    console.group('beforeUpdate 更新前状态===============》');
    console.log("%c%s", "color:red","el     : " + this.$el);
    console.log(this.$el);   
    console.log("%c%s", "color:red","data   : " + this.$data); 
    console.log("%c%s", "color:red","message: " + this.message); 
  },
  updated: function () {
    console.group('updated 更新完成状态===============》');
    console.log("%c%s", "color:red","el     : " + this.$el);
    console.log(this.$el); 
    console.log("%c%s", "color:red","data   : " + this.$data); 
    console.log("%c%s", "color:red","message: " + this.message); 
  },
  beforeDestroy: function () {
    console.group('beforeDestroy 销毁前状态===============》');
    console.log("%c%s", "color:red","el     : " + this.$el);
    console.log(this.$el);    
    console.log("%c%s", "color:red","data   : " + this.$data); 
    console.log("%c%s", "color:red","message: " + this.message); 
  },
  destroyed: function () {
    console.group('destroyed 销毁完成状态===============》');
    console.log("%c%s", "color:red","el     : " + this.$el);
    console.log(this.$el);  
    console.log("%c%s", "color:red","data   : " + this.$data); 
    console.log("%c%s", "color:red","message: " + this.message)
  }
}
</script>
<style scoped>
</style>

    最终控制台打印:

    


    根据这个打印结果,我(哔哔哔)的研究不下去了!

    说好的挂载前,要把虚拟dom放上去的呢???此处不应该是 <div class=""><div>{{ message }}</div></div>嘛???

    ok,基于这个问题的答案其实很简单,vue-router作为单页面应用,最终都会将子页面发给<router/>,所以这里的this指向的是vue.component,也就是其中的一个子页面的组件实例,而不是全局的vue。

    好的,现在知道哪儿出了问题。但是,官网并没有给为什么子组件初始化的时候在挂载dom前这个生命周期为什么没有虚拟dom的理由。那只能猜了(当然啃源码只坠吼滴)。

    1.个人认为比较扯淡的理由,在vue官方论坛看到的,但讲的还不错,如果你想获取$el,你可以去mounted状态获取,所以不用beforeMount状态就好了,在实际开发过程中,确实很少使用这个生命周期,因此觉得这么解决好像也能敷衍过去。

    2.自己结合官方API猜的

    官网API中一句话:注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted

    刚才已经说到,vue-router作为单页面应用,每个页面其实就是一个组件,而且每个页面中的this指向本身,也就是子页面自己,而不是组件的父级,当然你可以通过this.$parent.$parent(这里我没有打错,就是要找父级的父级,没搞明白为什么,有大佬知道的告诉我下)去访问全局的vue。当你访问this.$parent.$parent.$el的时候你可以发现beforeMount的时候子组件并没有被挂载,作为单页面应用,每一个子页面的虚拟dom应该交由他的父级去处理,因此在子组件内部的beforeMount状态中还没有关于$el的描述也是情理之中的。

    不管怎么说,尽量不要用beforeMount就好了。

    关于另外两个状态,更新数据和更新dom完毕,在vue检测到数据改变时就会触发,但搜集大部分使用者的建议是,用watch、computed等方法去监控或处理数据,尽量不要在这两个状态去变更数据,我同意大部分人的看法,所以这两个状态就不详细介绍了,实际生产过程中也很少用到。


我不敢保证上面那部分说的是对的,但我敢保证你继续往下看会发现更多错

    刚才我说道,vue的生命周期可以是顺序执行的,也可以不是,刚才讲的是顺序执行的情况,下面来说一说异步数据加载的原理以及带来的各种负面影响。

    先上一段代码,在刚才的基础上+一两句

//data字段有个num 
created: function () {
    console.group('created 创建完毕状态===============》')
    console.log('%c%s', 'color:red', 'el     : ' + this.$el) // undefined
    console.log('%c%s', 'color:red', 'data   : ' + this.$data) // 已被初始化
    console.log('%c%s', 'color:red', 'message: ' + this.message) // 已被初始化
    //新增代码片段
    setTimeout(() => { //这里只是为了偷懒用了ES6的箭头函数,如果是普通函数请注意this指针修改,vue中请不要滥用箭头函数,出了问题找都找不到
      this.num ++
      this.num += 2
    }, 0) //注意这里的延时都是0
    setTimeout(() => {
      this.num -= 5
    }, 0)
 }

    控制台打印结果:

    

请配合官方异步更新原理和打印结果细细体会,建议先把个人理解的通俗版的看完,再看官方的解释

异步更新队列

可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

    首先可以注意到的点就是,程序并不会顺序执行,怎么说呢,我们可以从控制台打印看到,vue在执行代码的时候,并没有去管定时器里发生了什么事情,甚至我已经设置了0延时,他依旧会去顺序执行其他生命周期,看起来就像跳过了这些异步加载,当我们访问接口的时候,这时我们就开启了一个异步加载,发生的事情和上面的差不多。因此可以确定的一个点就是,生命周期中的任何异步操作,都不会按照顺序执行,他会在非异步操作结束后执行,因此书写这部分代码的时候请注意里面的逻辑不要和顺序挂钩,要确保任何异步操作即使最后执行,之前的程序也不会发生异常从而阻塞整个进程。

    这样说起来可能有点复杂,这里我举一个vue用起来特特特特别蛋疼的地方你们就明白了.

<span>{{person.name.firstName}}</span>
<span>{{person.name.firstName}}</span>  
data: function () {
    return {
      message: 'hello world',
      add: 1,
      person: {
      }
    }
  },
created: function () {
    console.group('created 创建完毕状态===============》')
    console.log('%c%s', 'color:red', 'el     : ' + this.$el) // undefined
    console.log('%c%s', 'color:red', 'data   : ' + this.$data) // 已被初始化
    console.log('%c%s', 'color:red', 'message: ' + this.message) // 已被初始化
    //假装接口返回了一些信息给你,如一个人,然后你把这些信息赋值给了this实力
    setTimeout(() => {
      this.person = {
        name: {
          lastName: 'carry',
          firstName: 'dong'
        },
        sex: '男'
      }
    }, 0)

    上面的程序控制台会报错,啥错呢?vue说我解析this.person.name.firstName的时候发现他undefined

    

    你(哔哔哔)是不是瞎,我明明在created的时候把person信息给你了呀,你竟然说你mounted阶段解析数据的时候解析不出来,菜鸡!

    发生这个问题的原因就是,访问接口是一个异步操作,当程序顺序执行生命周期的时候,并不会理你这个异步操作,打个比方,生命周期和异步操作就像浏览器的两个工厂,两个工厂以及里面的车间可以一起开工,但是生命周期车间享有,我没完工,你异步操作就必须等我搞完的权利,因此,不管异步操作是否可以在生命周期结束前执行回调,他都只能乖乖的排在后面等待。因此,当解析器解析到this.person.name.firstName的时候他还真是个undefined,所以遇到类似问题,没别的办法,你只能乖乖的把变量的所有字段预先定义好(当然你也可以不理会报错,因为从结果上来看,程序还是会正确执行,但程序员或多或少都有点强迫症,看到那种红色字体就浑身难受)。至于为什么要这样管理,就跟浏览器的单线程处理机制有关了,你可以理解为,这两个工厂有一个共同的管理员(处理器),这个管理员同时只能管理一个工厂,而且这个管理员特别偏心,他都是等javaScript普通操作全部完工了,再去处理异步车间的事情,因为异步车间往往比普通车间消耗的时间要长,这就好像小学时候老师从隔壁办公室进教室抓讲话的人,每次就抓我,哪怕我有时候没讲话,就很气,但是并没有什么办法~

    如果把所有的异步操作看作一个工厂的话,那么不同的异步操作就好像工厂里不同的车间,他们可以同时干活,而且同一个车间里的活只汇报一次,哪怕你老是改订单量,但最终这个车间汇报的都是最后的结果,不会因为你改了一百次订单量,我就汇报一百次,那管理员没那么空,同时车间汇报也按照订单完成顺序排队,即便同时完成,也要等管理员处理完其中一个,下一个才能进去,这就是单线程的蛋疼之处,当然单线程也有很多好处,有兴趣的可以自己了解一下。

    关于生命周期和异步加载就理解到这儿,有错可以提醒我,毕竟我也是个小白~


    




猜你喜欢

转载自blog.csdn.net/dkr380205984/article/details/81059765