事件循环:微任务与宏任务

事件循环:微任务与宏任务

JavaScript的执行流,无论是浏览器还是Node.js,都是基于事件循环

理解事件循环能够让我们写出更可靠的高性能代码。

让我们先介绍一下事件循环的原理,然后再来看看实际应用。

事件循环(Event Loop)

事件循环的概念非常简单。它就是一个无止境的循环,JavaScript引擎等待任务(tasks)出现,然后执行任务,执行完毕后继续等待任务出现。

JavaScript引擎对事件循环的算法为:

  1. 当发现任务时:

    • 执行任务,从最先进入队列的任务开始
  2. 等待其他任务出现,然后执行步骤1。

当浏览网页时,就是以这种方式呈现。JavaScript引擎在大部分时间都处于空闲状态,仅在被脚本文件、处理函数,事件系统激活时才运行。

比如:

  • 当通过

    ...但是假如我们想在任务执行过程中展示一些额外信息,例如进度条。

    如果我们把这些‘重’的任务通过setTimeout分离成一个一个的小任务,那么对i的改变会不断地被渲染出来。

    例如:

    现在,div元素会显示不断增加的i的值,像极了进度条。

    使用案例3:在事件以后做些什么

    在一个事件处理函数中,我们可能会延缓执行某些操作,直到事件冒泡完成并且被所有事件阶段所处理。我们可以通过将某些操作包裹在0秒延迟的setTimeout中。

    因为原文案例设计一些额外知识(CustomeEvent 自定义事件),为了简单起见,此处为作者提供案例

        

    宏任务和微任务

    除了在上文提到过的宏任务外,还存在着微任务(microtasks)

    异步任务需要更准确地管理。所以,ECMA标准定义了一个内部队列promiseJobs,更多地被称作是“微任务队列(microtask queue)”(ES8术语)。

    如ESMA262中所定义:

    • 微任务队列为“先进先出”:最先进入队列的任务最先执行
    • 只有当目前没有运行其他任何任务时,微任务才会开始执行

    简单来说,当一个Promise已就绪,它的.then/catch/finally事件处理函数将被放入队列;它们暂时不会被执行。只有当JS引擎处理完当前的代码,才会按照顺序执行微任务队列中的任务。

    例如:

    let promise = Promise.resolve();
    
    promise.then(() => alert("promise done!"));
    
    alert("code finished"); // this alert shows first

    执行上面代码,code finished将会先显示,然后是promise done!

    事件循环:微任务与宏任务_第2张图片

    Promise函数永远会按照这个顺序执行。

    如果是链式的.then/catch/finally,那么会异步地执行每一项。也就是说,先将他们放入微任务队列,然后等待当前代码执行完毕,并且微任务队列中前面的任务执行完毕,然后执行。

    如果需要按照顺序执行呢?如果确保promise done先显示,然后才是code finished呢?

    只需要通过.then来将它们依次放入队列:

    Promise.resolve()
      .then(() => alert("promise done!"))
      .then(() => alert("code finished"));

    微任务来自于我们的代码。通常是通过Promise创建,.then/catch/finally的处理函数成为一个微任务。同样,await函数也适用,它是另一种Promise的处理方式。

    另外,通过queueMicrotask(func)函数可以将func这个函数放入微任务队列(目前IE还不支持)。

    在每一个宏任务执行完毕后,引擎会立即执行微任务队列中的所有任务,然后继续执行其他宏任务或渲染DOM操作。

    例如:

    setTimeout(() => alert("timeout"));
    
    Promise.resolve()
      .then(() => alert("promise"));
    
    alert("code");

    上面的代码中弹框将会按照什么顺序显示呢?

    1. 先显示code,因为它是一个普通的同步函数;
    2. 然后显示promise,因为.then处于微任务队列中,所以当当前宏任务执行完毕就会执行;
    3. 最后显示timeout,因为它是另一个宏任务。

    完整的事件循环流程:(从上往下,脚本文件(宏任务) -> 微任务 -> 渲染操作 -> 重复流程...)

    事件循环:微任务与宏任务_第3张图片

    在任何其他的事件处理函数、渲染操作或其他宏任务执行之前,所有的微任务都会执行完毕。

    如果我们希望去异步地执行一个函数(当前代码执行完毕后),但是在DOM操作被渲染之前,或者其他事件处理函数、宏任务执行之前,可以通过queueMicrotask来设置。

    另一个进度指示条的例子:和上文中提到的那个类似,但是在这里用的是queueMicrotask,而不是setTimeout。在每一个宏任务执行完毕后都会进行渲染操作,就好像是同步代码:

    总结

    更多关于事件循环算法的详细信息(和事件循环定义来比,仍然是很简单的):

    1. 宏任务中最先进入的任务最先出列并且执行(例如脚本文件);
    2. 执行所有微任务:

      • 当微任务队列不为空时:

        • 微任务队列中最先进入的任务出列并且执行
    3. 执行渲染操作(如果有对DOM进行修改的话);
    4. 如果宏任务队列为空的话,等待宏任务出现;
    5. 执行步骤1。

    如果要设置一个新的宏任务:

    • 使用0秒延迟setTimeout(f)

    当把一个涉及大量运算的任务分离成一个一个的小任务时,设置新的宏任务就能够使得浏览器能够对用户的操作做出响应并展示进度。

    同样可用于事件处理函数,当事件被完全处理完毕(事件冒泡完毕)后执行一个操作。

    如果要设置一个新的微任务:

    • 使用queueMicrotask(f)
    • 使用Promise,promise任务会处于微任务队列中。

    在微任务队列处理期间,任何UI或者网络事件都不会被处理,微任务队列中的所有任务会一个一个立即执行。

    所以我们可能会用queueMicrotask去异步执行一个函数,但是当前的环境状态(environment state)还没有被改变。

    Web Workers
    如果不想阻塞事件循环,在涉及到非常大的复杂运算时,可以使用 Web Workers
    通过并行线程的方式来运行代码
    Web Workers能够与主过程交换信息,但它拥有自己的变量、事件循环
    Web Workers不能访问DOM,所以在进行计算时同时使用多核CPU是非常有用的。

你可能感兴趣的:(事件循环:微任务与宏任务)