事件循环机制出现的原因

我们知道,浏览器是一个单线程的语言,并且他的JS引擎线程和页面渲染的GUI渲染线程是互斥关系。如果JS中出现一些耗时操作(比如说定时器,网络请求等),不可避免的会导致页面加载的阻塞。
此时出现了同步任务和异步任务的概念,同步任务放入主线程执行,JS引擎碰到异步任务后将其挂起,将回调函数转入事件触发线程,待到异步任务到达了执行时机之后,事件触发线程将其插入到事件触发线程维护的任务队列,等待主线程的执行。这就是事件循环机制的基本逻辑。

宏任务和微任务

javascript中,异步任务分为宏任务和微任务。其实浏览器内部,维护了两个队列:

  1. 一个是我们上面所说的事件触发线程维护的任务队列(**task queue**),也叫宏任务队列。
  2. 一个是JS引擎线程维护的微任务队列(microTask queue)。(至少在chrome中是V8引擎在维护着这个微任务队列。)

    宏任务

    位于任务队列中的任务都属于宏任务。它的来源很多:

  3. setTimeout/setInterval

  4. 整体的script块代码也被当成一个宏任务。
  5. 事件监听的回调函数。
  6. 监听XHR对象状态变更的回调函数。

    微任务

    位于微任务队列中的任务被称为微任务。

  7. Promise.then的回调函数

  8. MutationObserver
  9. window.queueMicrotask

    为什么有了宏任务后,又要有微任务

    众所周知,队列是先入先出的。后进入队列的任务想要提高他的执行优先级是无法办到的。如果我们想要某些任务晚于同步任务执行,却希望比异步任务和页面渲染的优先级要高一点,这个时候微任务的概念就诞生了。其实他属于异步任务的特权阶级。

    回头再看事件循环机制

  10. 将整体代码当成一个宏任务推入主线程中执行。

  11. 碰到宏任务,将其挂起,将回调函数交给事件触发线程,当达到任务执行的条件之后,事件触发线程将其插入到任务队列的尾部,等待主线程的执行。在这里,不同的宏任务也涉及到了不同的线程相互配合共同完成工作的目标。
    1. 对于setTimeout/setInterval等定时任务,它是由定时触发器线程来进行计时的,当计时完毕后,就达到了该宏任务的执行时机,则通知事件触发线程将任务插入到任务队列中。
    2. 对于网络请求等异步任务,当XHR对象建立tcp连接后,就会开辟一个异步请求线程,完成网络的请求。它会监听xhr对象状态的变更,如果状态进行了变更,就会通知事件触发线程将xhr对象状态变更的回调函数插入到任务队列。
  12. 碰到微任务,也是将其挂载。达到微任务的执行时机后,就将相应的回调函数插入到微任务队列,比如Promise对象,状态从pending变成了fulfilled/rejected等等。
  13. 当主线程执行完同步代码后,就会去查看微任务队列,对微任务队列进行清空。出栈进入主线程被JS引擎执行。当微任务执行的过程中,又产生了新的微任务。它会进入微任务队列尾部,同样会在这次事件循环中。被执行。直到整个微任务队列被清空。
  14. 此时,会去检查页面是否需要进行渲染,这是因为JS引擎在执行的时候,负责渲染的GUI渲染线程是被冻结了的,当存在渲染任务,比如说重绘,回流等任务的时候,会将其维护在一个队列中,等主线程空闲的时候,执行渲染。
  15. 此时,可以说是完成了一次事件循环(event loop)。此时,JS引擎就会去宏任务队列中,d队列首部取出一个任务开启下一次事件循环。

    requestAnimationFrame的执行时机

    根据MDN的定义,它会在下一次重绘的时候,执行你传入的回调函数。
    1. requestAnimationFrame(() => {
    2. console.log("requestAnimationFrame");
    3. })
    重绘的执行时机是微任务执行完毕后,GUI渲染线程接管渲染的时候,所以我个人认为它的执行时机是微任务队列清空完毕后,浏览器渲染开始之前。所以它的优先级会低于微任务。高于宏任务。

    queueMicrotask方法

    通过这个方法可以向微任务队列中添加一个回调函数。
    1. queueMicrotask(() => {
    2. console.log("微任务");
    3. })