背景与总结

先说结论:

有很多所谓的最佳实践告诉你这个观点,但其实这个观点是非常片面的,甚至在大部分场景下他是错的。

声明一下:

本文通过几个案例说明部分同行观点的片面性,分析了这种观点出现原因,涉及一点vue的渲染原理。最后给出作为个人为了避免困扰,可以采取的相对最近实践(因人而异)。

分析

很多文章给出的理由是:mounted回调函数被调用的时候,组件已经被挂载到了DOM上,而比mounted更早的生命周期还没有挂载。

这句话本身没有毛病,但它和mounted里执行异步请求没有必然的因果关系。

关键就在于,mounted回调执行时,组件已经挂载到了DOM上,可我们的异步请求与DOM有什么关系吗?

并没有直接的关系,虽然我们请求的结果需要通过DOM呈现在界面上,用户才能看到。

但是,别忘了,通常我们的请求结果是经过处理然后复制给data的一个属性,就像这样:

getData(){
  axios.get(url).then(response => {
    // 这里只是一个复制操作
    this.dataset = response.data
  })
}

我们只是做了一个赋值操作,并没有操作DOM
之后这个赋值操作会被vue拦截到,然后去调用render函数生成新的vnode,进一步去做diff找出需要更新的属性、节点、样式等,最后会有真正的读写DOM的操作。

这也正是为什么都说要放在mounted回调中执行异步请求的原因,因为赋值操作会导致DOM的操作。

但这个理由根本不成立,关键就在于异步这俩字。

如果你要做一个同步的请求,那么我同意上述的观点,但不好意思,你不是同步的。

案例

来看两个例子对比下:

第一个,我们在created中有个异步操作,修改了requested属性。

<template>
  <h1>{{ requested }}</h1>
</template>

<script>
export default {
  data() {
    return {
      requested: '',
    }
  },
  created() {
    console.log('created')
    setTimeout(() => {
      this.requested = 'done!'
      console.log('setTimeout!')
    })
  },
  mounted() {
    console.log('mounted')
  },
}
</script>

图2

结果:代码正常运行,界面正常展示,并没有发生你所担心的created中读写DOM出错的问题。而且从输出的日志可以看出来,异步回调是在mounted之后才执行的,这个很关键。

如果换成同步操作呢?

<template>
  <h1>{{ requested }}</h1>
</template>

<script>
export default {
  data() {
    return {
      requested: '',
    }
  },
  created() {
    console.log('created')
    this.requested = 'requested finished!'
    console.log('requested finished!')
  },
  mounted() {
    console.log('mounted')
  },
}
</script>

image.png

结果:代码正常运行,界面正常展示,也没有发生你所担心的created中读写DOM出错的问题。
而从输出的日志可以看出来,created中的同步操作是在mounted之前执行的。
这里很奇怪,尽管我们修改了data中的一个状态会导致vue去读写DOM,但此时并没有发生错误。

Vue渲染原理(部分)

实际上,这里需要简单了解下vue的渲染流程-diffdiff会对oldVnodenewVnode进行对比,在开始的时候有个很简单的逻辑,以下是伪代码:

if (!oldVnode){
  // 没有上一次的 vnode,说明是首次渲染
  // 直接执行挂载方法,不需要再进行其它的比较
  mountComponent()
} else {
  // 正常的属性、子节点等的对比
}

所以,created中同步的修改状态不用担心读写DOM出错的问题。

异步请求放在哪里

业内同行给出mounted中执行异步代码的建议是相对安全的一种选择,对于部分初学者来说,避免了业务开发的难度,避免了疑难问题的出现频率。

由于前端各种开发人员对vue的原理认识不足,凭着自身的经验给出这个最佳实践是可以理解的。

实际上,通过上面的案例可以看到,将异步/同步请求放在created里执行是可以的,当然有个前提哈:请求的回调中不能直接读写DOM
甚至,你异步(同步不行)请求放在beforeCreate中执行都是可行的,请求的回调中不能直接读写DOM依然是前提。

<template>
  <h1>{{ requested }}</h1>
</template>

<script>
export default {
  data() {
    return {
      requested: '',
    }
  },
  beforeCreate() {
    console.log('beforeCreate')
    setTimeout(() => {
      this.requested = 'done!'
      console.log('setTimeout!')
    })
  },
  created() {
    console.log('created')
  },
  mounted() {
    console.log('mounted')
  },
}
</script>

结果:
image.png

代码执行运行,界面正常展示,很匪夷所思吧。

哈哈,有个重要的点,vue中的渲染流程initbeforeCreatecreatedbeforeMountmounted等生命周期钩子的调用是同步的,所以异步的请求回调都会在这些同步钩子执行完后才可能执行(可能的意思是,还得等其它同步任务执行完成)。

这里的原理涉及到js的事件循环,不过多展开了,有兴趣可以看看这篇文章【JS】深入理解事件循环,这一篇就够了!

另外,以上分析案例都是基于vue2.x的版本,实际上,使用vue3.x,结论依然成立。

综上所述

  • 如果你是初学者或者只是临时使用vue实现简单的业务,不想过多的深入,那么直接在mounted中执行异步代码一般没有明显的问题;
  • 如果你是追求更好的性能,以及更深入的理解vue,那么不妨更深入的思考下最佳实践原因,它是否适合你以及你的项目呢。

lpicker
87 声望1 粉丝