JavaScript运行时

在执行 JavaScript 代码的时候,JavaScript 运行时实际上维护了一组用于执行 JavaScript 代码的代理。每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 worker 的额外的线程集合、一个任务队列以及一个微任务队列构成。除了主线程(某些浏览器在多个代理之间共享的主线程)之外,其它组成部分对该代理都是唯一的

事件循环(Event Loops)

每个代理都是由事件循环驱动的,事件循环负责收集用事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。(本节文字来自mdn,个人觉得描述有误,具体顺序往下看)

Event Loops中,每一次循环成为tick,每次tick的流程如下:

  • 执行主线程中的同步代码(可以看做一个宏任务),直到执行完成
  • 检查微任务队列,若不为空,则执行微任务队列中的所有微任务,直到队列为空
  • 检查宏任务队列中在当前tick之前加入到队列中的宏任务,依次执行
  • 没执行完一个宏任务,重复依次第二个步骤
  • 当微任务队列已清空且宏任务队列中当前tick之前加入的宏任务都已执行完成,执行一些别要的渲染和绘制工作(宿主环境为浏览器时)
  • 结束当前tick,进入下一个tick

分类

  • Window 事件循环
  • Worker 事件循环
  • Worklet 事件循环

宏任务&微任务

一个任务(宏任务)就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。 除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务。
起初微任务和任务之间的差异看起来不大。它们很相似;都由位于某个队列的 JavaScript 代码组成并在合适的时候运行。

宏任务和微任务的区别

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

创建途径

宏任务:

  • 程序的初始化
  • 事件触发的回调
  • setTimeout()/setInterval()
  • postMessage, MessageChannel
  • setImmediate() node环境

微任务:

  • Promise
  • MutaionObserver
  • Object.observe(已废弃,被Proxy对象替代)
  • process.nextTick() node环境
  • queueMicrotask()

代码示例

setTimeout(() => {
    console.log(3);

    // 在执行宏任务时加入宏任务,加入的宏任务会在下个tick执行
    setTimeout(() => {
        console.log(8);
    });

    // 在执行宏任务时加入微任务,当前宏任务执行完成后,立即执行该微任务
    new Promise(resolve => {
        resolve();
        console.log(4);
    }).then(() => {
        console.log(6);
    });

    // 在执行宏任务时加入宏任务,加入的宏任务会在下个tick执行
    setTimeout(() => {
        console.log(9);
    });

    console.log(5);
});

setTimeout(() => {
    console.log(7);
});

new Promise(resolve => {
    resolve();
    console.log(1);
}).then(() => {
    console.log(2);
});

// 输出结果:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

参考文档:


feipeng123s
16 声望3 粉丝

引用和评论

0 条评论