异步加载在Vue生命周期哪个阶段更合理

7,071 阅读2分钟

react高阶面试题中有这么一道:为什么异步请求数据在didMount阶段更合适?同为MVVM中的翘楚,Vue是否也有类似问题呢?另外,我在平时也无开发过程中也会发现,每个人选择的那个生命周期阶段去异步请求数据总会不一样,因此引发思考,到底哪个阶段更适合异步请求数据呢?在产品设计和用户体验方面又会有哪些影响?本篇记录就是为了解决这两个问题。

一、Vue生命周期

首先再老话重提过一下Vue生命周期,以及每个阶段都做了什么事。

1. beforeCreated:生成$options选项,并给实例添加生命周期相关属性。在实例初始化之后,在 数据观测(data observer) 和event/watcher 事件配置之前被调用,也就是说,data,watcher,methods都不存在这个阶段。但是有一个对象存在,那就是$route,因此此阶段就可以根据路由信息进行重定向等操作。

2. created:初始化与依赖注入相关的操作,会遍历传入methods的选项,初始化选项数据,从$options获取数据选项(vm.$options.data),给数据添加‘观察器’对象并创建观察器,定义getter、setter存储器属性。在实例创建之后被调用,该阶段可以访问data,使用watcher、events、methods,也就是说 数据观测(data observer) 和event/watcher 事件配置 已完成。但是此时dom还没有被挂载。该阶段允许执行http请求操作。

3. beforeMount:将HTML解析生成AST节点,再根据AST节点动态生成渲染函数。相关render函数首次被调用(划重点)。

4. mounted:在挂载完成之后被调用,执行render函数生成虚拟dom,创建真实dom替换虚拟dom,并挂载到实例。可以操作dom,比如事件监听

5. beforeUpdate:$vm.data更新之后,虚拟dom重新渲染之前被调用。在这个钩子可以修改$vm.data,并不会触发附加的冲渲染过程。

6. updated:虚拟dom重新渲染后调用,若再次修改$vm.data,会再次触发beforeUpdate、updated,进入死循环。

7. beforeDestroy:实例被销毁前调用,也就是说在这个阶段还是可以调用实例的。

8. destroyed:实例被销毁后调用,所有的事件监听器已被移除,子实例被销毁。

总结来说,虚拟dom开始渲染是在beforeMount时,dom实例挂载完成在mounted阶段显示。

那么接下来了解就是render函数。

render示例:export default {
   data () {
       return {
           menu_items: []   // 请求返回如:[{fullname: '页面一'},{fullname: '页面二'},{fullname: '页面三'},{fullname: '页面四'}]
       }
   },   render (createElement){    return createElement(
          // 1. 第一个参数,要渲染的标签名称(必填)
          'ul',
          // 2. 第二个参数,1中要渲染的标签的属性,或者文本元素(可选)      {  

               class: {'uk-nav': true},           },          // 3. 第三个参数,1中标签的子元素,详情看官方文档(可选)
        this.menu_items.map(item=>createElement('li',item.fullname)))

       )  }}

render函数最终返回的是createNodeDescription(节点描述),即俗称virtual node(虚拟节点)。用template写的话,就是下面这样:

<template>

    <ul>       <li v-for="item in menu_items">         {{ item.fullname }}     </li>  </ul>

</template>

这个过程在mounted被调用前完成。详细参考可移步 这里

二、异步加载

setTimeout等异步函数

异步函数跟同步函数的不同之处,最大的应该就是异步函数会等到所有同步函数执行完成之后再执行。具体的可以看 事件循环

//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在执行代码的时候,并没有去管定时器里发生了什么事情,甚至已经设置了0延时,他依旧会去顺序执行其他生命周期,看起来就像跳过了这些异步加载。因此可以确定一点,生命周期中的异步操作不会按照顺序执行,而是会等到非异步操作结束后执行。因此书写这部分代码的时候请注意里面的逻辑不要和顺序挂钩,要确保任何异步操作即使最后执行,之前的程序也不会发生异常从而阻塞整个进程。

ajax异步请求

ajax请求是异步操作,回调函数的执行时间是不确定的。也就是说,即使在created钩子发送请求,dom被挂载之后请求仍没有返回结果,就很有可能导致运行出错,诸如:


因为此时上述render事例中的menu_items还是空置。

解决方案

针对ajax异步请求,这样的错误原因其实就是因为返回结果没赶上dom节点的渲染。所以可以从两方面做修改:一是返回结果的赋值变量上,另一个就是dom节点的渲染层面。

1. 给予赋值变量初始值,即定义时menu_items:[ {fullname: ''} ]。

这么做的好处就是页面节点的渲染不受限于返回结果,静态文案照样会被渲染,动态数据则会在数据更新时被填充。给用户的感觉就是,页面渲染速度不错。

但是这种方式也有缺陷,后台返回数据字段不尽相同,要是都这么写那就真是麻烦了。

当然如果你使用typescript就没有这种烦恼,menu_items: { [propName: string]: any } = {}就搞定了。

2. v-if,控制dom节点的挂载,当且仅当menu_items被赋予返回值时,才开始渲染节点。

这么做的好处就是静态和动态文案同步展现在用户面前,不会有文案跳动,数据从无到有的过程。但是,副作用就是页面渲染时间、用户等待时间变长。

那如果dom挂载前请求数据已经返回了,又会是怎样的结果呢?

我们可以用setTimeout来模拟一下这个过程

<span>{{person.name.firstName}}</span> 

data: function () {
    return {
      message: 'hello world',
      add: 1,
      person: {
        name: {}
      }
    }
  },
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)

请求够早了吧,但还是报错了,this.person.name.firstName 是 undefined,不过程序报完错后还是再继续执行了。

三、结论

既然异步函数并不会阻塞vue生命周期整个进程,那么在哪个阶段请求都可以。如果考虑到用户体验方面的影响,希望用户今早感知页面已加载,减少空白页面时间,建议就放在created阶段了,然后再处理会出现null、undefined这种情况就好。毕竟越早获取数据,在mounted实例挂载的时候渲染也就越及时。

当然即使是这种情况下,也不排除会触发updated生命钩子(data有默认值且已渲染,之后数据被更新),从而导致虚拟dom的重新渲染。