event-loop 宏任务 微任务

从一道执行顺序的面试题,到最终搞懂这一块,记录下来,这是一些自己的见解,可能不是那么准确,但是确实能让我自己理解这一块内容了。

还有requestAnimation一直跟宏、微分不开,所以也放到这一篇来科普

浏览器机制

浏览器运行机制

  1. 用户界面:除了浏览器主窗口显示的请求界面外,都属于用户界面。
  2. 浏览器内核:在用户界面与渲染引擎之间传送指令。
  3. 渲染引擎:负责显示请求内容,解析 HTML 和 CSS,并将解析结果传送到界面上。
  4. 网络:用于网络调用,如 HTTP 请求。起接口与平台无关,为所有平台提供底层实现。
  5. 用户界面后端:用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。(个人理解是一些获取用户习惯的接口)
  6. JavaScript 解释器:用于解析和执行 JavaScript 代码。
  7. 数据存储:持久层。Cookie,session 等网络数据库,是一个完整且轻便的浏览器内数据库。

Nodejs 运行机制

  1. V8 引擎解析 JavaScript 脚本。
  2. 解析后代码调用 Node API。
  3. libuv 库负责 Node API 的执行。它将不同任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎
  4. V8 引擎再将结果返回给用户。

Nodejs 与任务队列相关的两个方法

  • process.nextTick
    • process.nextTick()的意思是定义一个动作,并让这个动作在下一次事件轮训的时间点上执行。
  • setImmediate:一次Event Loop执行完毕后调用。即当前轮询阶段完成后就执行函数。

宏任务与微任务

宏任务、微任务、事件循环之间的关系

  • 宏任务(macro-task):

    • 包括整体代码 script
    • setTimeout,setInterval->这个个人理解可以说是异步宏任务
    • requestAnimationFrame
    • setImmediate(Node 端)
  • 微任务(micro-task):

    • Promise.then、catch、finally
    • process.nextTick(Node 端)
    • MutationObserver

执行顺序:

主流程宏任务-> 异步微任务-> 异步宏任务

async\await

其实async\await本质上还是基于Promise的封装,而Promise是微任务的一种。所以在使用await关键字与Promise.then效果相似。

可以这样理解,在async函数中在await前都是同步执行的,await前的代码都是new Promise时传入的代码,然后执行await这一行的代码,再将await之后的所有代码都是在Promise.then中的回调。

配合代码理解

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout((_) => console.log(4), 0); // 注册异步宏任务

async function main() {
console.log(1);
await Promise.resolve();
// 以下注册微任务
console.log(3);
}

main();
console.log(2);
// 1,2,3,4
  1. 首先执行主流宏任务,首先是 1,然后是 2。
  2. 主流宏任务执行完
  3. 执行微任务,打印出 3
  4. 执行异步宏任务,打印出 4

requestAnimation

requestAnimationFrame 是什么

requestAnimationFrame方法告诉浏览器希望执行动画,并请求浏览器在下一次重绘之前调用回调函数来更新动画

回调函数

requestAnimationFrame的回调函数会被传入DOMHighResTimeStamp参数,DOMHighResTimeStamp指示当前被requestAnimationFrame()排序的回调函数被触发的时间。

语法

1
window.requestAnimationFrame(callback);
  • callback

下一次重绘之前更新动画帧所调用的函数,该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame()开始去执行回调函数的时刻。

  • 返回值

一个long整数,请求 ID,是回调列表中唯一标识。是一个非零值,没别的意义。可以传这个值给window.cancelAnimationFrame()以取消回调函数。

综合例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
async function async1() {
// 'async1 start'及await 相当于主流宏任务 (主宏-2)
console.log("async1 start");
await async2();

// await 后的代码都相当于Promise.then,放入微任务队列 (微-3)
console.log("async1 end");
}

async function async2() {
// 'async2'及await 相当于主流宏任务(主宏-3)
console.log("async2");
await Promise.resolve();

// 同上面(微-1)
console.log("async2 end");
}

// 主流宏任务(主宏-1)
console.log("script start");

// 异步宏任务 (异宏-1)
setTimeout(function () {
console.log("setTimeout");
}, 0);

// 主流宏任务(2)
async1();

new Promise(function (resolve) {
// (主宏-4)
console.log("promise1");
resolve();
})
.then(function () {
// 放入微任务队列 (微-2)
console.log("promise2");
})
.then(() => {
// 微任务队列(微-4)
console.log("promise end");
});

// 主流宏任务(主宏-5)
console.log("script end");

按照主流宏任务-微任务-异步宏任务的顺序 结果

1
2
3
4
5
6
7
8
9
10
script start(主宏-1)
async1 start(主宏-2)
async2(主宏-3)
promise1(主宏-4)
script end(主宏-5)
async2 end(微-1)
promise2(微-2)
async1 end(微-3)
promise end(微-4)
setTimeout(异宏-1)

这里为什么promise2async1 end前执行,是因为,async1 函数 调用async2函数,而async2中也有异步调用,所以相当于

1
2
3
4
5
6
7
8
9
10
11
12
function async1() {
new Promise(() => {
console.log("async1 start");
new Promise(() => {
console.log("async2");
}).then(() => {
console.log("async2 end");
});
}).then(() => {
console.log("async1 end");
});
}
-------------本文结束感谢您的阅读-------------