异步机制整理

起因

先看最近遇到的一段代码:

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
let res = (function () {
let timer;
function init() {
doit().then((_) => {
//微任务
console.log("ok");
timer && clearTimeout(timer);
});
}
function doit() {
timer = setTimeout(() => {
//创建宏任务
console.log("timeout");
}, 0);
return new Promise((resolve, reject) => {
for (let i = 0; i < 1000000; i++); //延长微任务时间
resolve();
});
}
return {
init,
};
})();

res.init(); //ok

这段代码的输出如上所示,虽然事后想明白了,但是当时还是犯了很蠢的问题,所以整理了一下有关于 js 的异步原理的整理。

什么是异步

js 作为单线程语言,如果同步执行则会造成非常严重的性能问题,所以在 js 设计中使用了异步机制,异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步执行机制:

所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,在”任务队列”之中放置一个事件。

一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

主线程不断重复

Event Loop

主线程从任务队列中不断读取事件,这个过程是循环不断的,此过程被称为 Event Loop

运行机制

在主线程运行时候,产生了堆和栈,其中,栈的代码调用了外部的 API,在任务队列中不断地加入时间,当栈中的代码执行完毕后,主线程会取执行任务队列,一次调用其中的回调函数,执行栈的同步任务总是会在异步任务前执行

宏任务与微任务

对于广义来说,可以定义同步任务和异步任务,但是对于异步还可以有更精细的划分,其中分为宏任务微任务

宏任务分类:定时器,整体 script 代码

微任务定义:Promise(then 的回调),process.nextTick()

对于同时存在宏任务和微任务的等待队列,首先会执行微任务,同时这也解释了开头那个问题中的定时器被清理的结果。

画个图来表示一下:

运行机制

对于定时器中的函数,作为宏任务,他的回调顺序是在微任务的回调之后的,这也就造成了定时器的代码无法顺利执行的结果。

Node 中的执行机制

Node.js 也是单线程的 Event Loop,但是它的运行机制不同于浏览器环境。

node运行机制

  1. V8 引擎解析 JS 脚本
  2. 调用 Node API
  3. libuv 库执行 Node API,同时,将不同任务分配给不同的线程,形成一个时间循环,以异步方式将任务执行结果返回给 V8 引擎
  4. V8 引擎将结果返还给用户

其中,Node 中提供了一些特殊的与任务队列有关的 API:process.nextTick 和 setImmediate

process.nextTick 方法可以在当前执行栈的尾部,下一次读取任务队列前触发回调

setImmediate 方法则是在当前任务队列的尾部添加事件

1
2
3
4
5
6
7
8
9
10
11
12
13
process.nextTick(function A() {
console.log(1);
process.nextTick(function B() {
console.log(2);
});
});

setTimeout(function timeout() {
console.log("TIMEOUT FIRED");
}, 0);
// 1
// 2
// TIMEOUT FIRED

如果有多个 process.nextTick 语句(不管它们是否嵌套),将全部在当前”执行栈”执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setImmediate(function A() {
console.log(1);
setImmediate(function B() {
console.log(2);
});
});

setTimeout(function timeout() {
console.log("TIMEOUT FIRED");
}, 0);

// 1
// TIMEOUT FIRED
// 2

我们由此得到了 process.nextTick 和 setImmediate 的一个重要区别:多个 process.nextTick 语句总是在当前”执行栈”一次执行完,多个 setImmediate 可能则需要多次 loop 才能执行完。事实上,这正是 Node.js 10.0 版添加 setImmediate 方法的原因,否则像下面这样的递归调用 process.nextTick,将会没完没了,主线程根本不会去读取”事件队列”!

查看评论