先了解Event Loop 事件循环机制

Event Loop说白了就是如何避免同一时刻操作同一个DOM节点而产生问题的方案机制,也是这时候js有了异步的概念

JavaScript原生

在JavaScript中,代码的执行是单线程的,即一次只能执行一个任务。但是,JavaScript也提供了一些异步编程的机制,例如定时器、异步函数等。这些机制会将一些任务放到异步队列中,等待执行。

Event Loop是JavaScript异步编程中的一个重要机制,它负责管理异步队列和执行任务。简单来说,Event Loop会不断地从异步队列中取出任务,执行它们,并在执行完毕后等待新的任务加入队列。

具体地说,Event Loop的工作原理如下:

  1. 执行同步代码,将异步代码放入异步队列中。
  2. 当异步代码执行完毕并有结果时,将回调函数放入回调队列中。
  3. 当异步队列中的所有任务都执行完毕后,开始执行回调队列中的回调函数。
  4. 重复上述步骤。

需要注意的是,异步队列和回调队列是不同的,异步队列是用来存放异步任务的,而回调队列是用来存放异步任务的回调函数的。在执行异步任务的过程中,如果异步任务中有需要执行的回调函数,它们会被放入回调队列中等待执行。

Vue3

在 Vue3 中,Event Loop 的机制与原生 JavaScript 中的机制类似,但是在 Vue3 中,还有一些额外的机制来处理异步更新和响应式变化。

在 Vue3 中,当响应式数据发生变化时,Vue3 会将更新任务放入一个队列中,并在下一个微任务中执行这些更新任务。这个队列被称为“更新队列”(Update Queue)。Vue3 使用 Promise-based 的异步机制,即使用 Promise.resolve().then() 来创建微任务。

Vue3 的 Event Loop 机制大致如下:

  1. 执行同步代码,将异步更新任务放入更新队列中。
  2. 执行完当前宏任务后,在下一个微任务中执行更新队列中的更新任务。
  3. 执行完所有的微任务后,开始执行下一个宏任务。

需要注意的是,Vue3 的更新队列只会在响应式数据发生变化时才会被触发,而不是每次 DOM 更新都会触发。这样可以减少不必要的更新,提高性能。

除了响应式数据的更新,Vue3 还提供了许多其它的异步机制,例如 nextTick、$nextTick 等。它们也遵循 Vue3 的 Event Loop 机制,将任务放入更新队列中,在下一个微任务中执行。

需要注意的是,Vue3 的异步机制有时会与原生 JavaScript 中的机制产生交互。例如,当使用 setTimeout 或 setInterval 时,这些任务会被放入宏任务队列中,而不是微任务队列中。这可能会影响到 Vue3 的更新队列的执行。为了避免这种情况,可以使用 Vue3 提供的 nextTick 或 $nextTick 方法来确保更新队列中的任务在下一个微任务中执行。

总之,Vue3 的 Event Loop 机制与原生 JavaScript 中的机制类似,但也有自己的特点和机制,需要注意其中的细节。

执行流程

image-20230402231145916

哪些同步/异步任务?

  • 同步任务:代码从上到下按顺序执行

  • 异步任务

    • 宏任务:script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax
    • 微任务:Promise.then catch finally、MutaionObserver、process.nextTick(Node.js 环境)

运行机制

  • 所有的同步任务都是在主进程执行的形成一个执行栈,主线程之外,还存在一个"任务队列",异步任务执行队列中先执行宏任务,然后清空当次宏任务中的所有微任务,然后进行下一个tick如此形成循环。
  • nextTick 就是创建一个异步任务,那么它自然要等到同步任务执行完成后才执行。

$nextTick()怎么保证的呢

Vue 3中的$nextTick()方法是在DOM更新完毕后执行回调函数,即在下次DOM update cycle之前运行。它可以用于确保当数据发生变化时,获取到正确的最新值以及确保某些异步操作完成后再进行下一步操作。Vue 3中使用该方法时需要通过导入@vue/runtime-core来获得该方法。例如:

import { nextTick } from '@vue/runtime-core'

// 在 DOM 更新后执行回调
nextTick(() => {
  // ...
})

具体的案例

假设有一个父组件包含多个子组件,在某个操作后需要依次访问所有子组件,则可以在该操作之后通过this.$nextTick()以确保每个具备特定方法且已经被完全渲染和挂载好的子节点都被访问了。

import { defineComponent, ref, nextTick } from 'vue'
import ChildComponent from './ChildComponent.vue'

export default defineComponent({
  components: { ChildComponent },
  setup() {
    const children = ref([])

    function handleClick() {
      // 操作后获取所有子节点
      children.value = $refs.childComponents

      // 使用 nextTick 确保_DOM更新和异步操作完成_
      nextTick(() => {
        // 访问每个已经完全渲染并挂载好孩纸们吧!
        children.value.forEach(child => child.foo())
      })
    }

    return {
      handleClick,
      children
    }
  }
})