事件循环是 JS 中重要的概念,用于处理同步代码、异步代码的执行顺序问题
因为 JS 的执行环境不同,分为浏览器事件循环和 Node.js
事件循环
浏览器事件循环比较简单
JS 的代码分为同步代码和异步代码,异步代码像这样
jssetTimeout(() => console.log(1)); // 像setTimeout这种函数的回调是异步的
console.log(2); // 普通的同步代码
JS 执行代码的顺序是,先清空执行栈,再执行异步代码,也就是同步代码先执行
我们再分析异步代码
首先我们把异步代码的回调分为:宏任务和微任务,同时有一条宏任务队列和一条微任务队列。整个事件循环的过程可以为以下几步
典型的宏任务和微任务如下
setTimeout
、setInterval
、requestAnimationFrame
Promise.resolve().then()
、await之后的代码
、MutationObserver
比如下面
jssetTimeout(() => console.log(1)); // 宏任务
Promise.resolve().then(() => console.log(2)); // 微任务
显然是 Promise.resolve().then()
先执行
其中有一种特殊情况,就是使用了 async await
关键字的函数,在使用 await
关键字之后的部分是异步代码
jsasync function async1() {
console.log('async1');
await async2();
// await 关键字之后的代码 可以 认为是Promise.then()
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1()
小试牛刀
试试多层嵌套的宏任务微任务
jsasync function fn() {}
setTimeout(() => {
console.log("Timeout 1");
Promise.resolve().then(() => {
setTimeout(() => {
console.log("Timeout 2");
}, 0);
console.log("Promise 1");
});
}, 0);
Promise.resolve().then(() => {
console.log("Promise 2");
setTimeout(async () => {
console.log("Timeout 3");
await fn();
console.log("await 1");
setTimeout(() => {
console.log("Timeout 4");
}, 0);
}, 0);
});
可以尝试模拟任务插入队列,推出队列到执行栈中执行的过程
// 以下p1 t1 等为简写 (x) 表示已推出队列 // *1* // 微任务:p2(x) // 宏任务:t1 t3 // 输出:p2 // *2* // 微任务:p2(x) p1(x) // 宏任务:t1(x) t3 t2 // 输出:p2 t1 p1 // *3* // 微任务:p2(x) p1(x) a1(x) // 宏任务:t1(x) t3(x) t2 t4 // 输出:p2 t1 p1 t3 a1 // *4* // 微任务:p2(x) p1(x) a1(x) // 宏任务:t1(x) t3(x) t2(x) t4(x) // 输出:p2 t1 p1 t3 a1 t2 t4
Node.js
的运行机制如下:
V8
引擎解析 JS 脚本Node API
libuv
负责 Node API
的执行,它将不同任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给 V8
引擎V8
引擎再将结果返回给用户Node.js
中会复杂一,同样还是宏任务、微任务,但是宏任务又分为几个阶段,如下
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
一般是从输入数据开始整个事件循环
常见的几个阶段对应的 api
有
poll
:一些 IO 操作,比如 fs.readFile
check
: setImmediate
close callback
: 一般不考虑,但是可以知道 readStream.close
timers
:setTimeout
、setInterval
pending callbacks
:执行推迟到下一次循环迭代的 I/O 回调,比如 tcp 错误回调idle, prepare
:仅 Node.js
内部使用比如下面代码
jssetImmediate(() => {
console.log('check');
})
setTimeout(() => {
console.log('timer');
})
fs.readFile('./index.js', () => {
console.log('poll');
})
// 执行结果:
// timer
// check
// poll
这里顺序基本和上面一样,但是令人疑惑的是,为什么 poll 在 check 之后执行,不是说 poll -> check 吗
实际上如果是正常的同步代码,那么 poll 的回调应该马上交给队列,但是因为需要 libuv 读取文件,因此 fs.readFile
的回调延迟交给了队列,所以先执行了 timer 和 check
比如像下面这种情况,执行顺序就和上述的一样了
jsfs.readFile("./index.js", () => {
setTimeout(() => {
console.log("timer");
});
setImmediate(() => {
console.log("check");
});
console.log("poll");
});
// 执行结果
// poll
// check
// timer
对应宏任务的每个阶段,它们都是使用队列管理的,比如下面代码
jsfs.readFile("./index.js", () => {
setTimeout(() => {
console.log("timer1");
fs.readFile("./index.js", () => {
console.log("poll2");
});
setImmediate(() => {
console.log("check3");
});
});
setTimeout(() => {
console.log("timer2");
});
setImmediate(() => {
console.log("check1");
fs.readFile("./index.js", () => {
console.log("poll1");
});
});
console.log("poll");
setImmediate(() => {
console.log("check2");
});
});
// 输出结果
// poll
// check1
// check2
// timer1
// timer2
// check3
// poll1 这两个poll的顺序不一定
// poll2
像这里,只要是同一个事件循环内 push 到队列中的任务,都需要清空相应的队列之后才会执行下一阶段队列,比如 check
和 timer
这两个阶段
而其中的 poll 因为需要 libuv 读取文件处理,有一定的延时,因此几乎不用考虑队列,因为基本不太可能两个 poll 任务在同一个事件循环内执行
Node.js
环境中,微任务还有一种叫做 process.nextTick
,Node.js
官网给出的解释是
Looking back at our diagram, any time you call
process.nextTick()
in a given phase, all callbacks passed toprocess.nextTick()
will be resolved before the event loop continues. 回顾我们的图表,任何时候在给定阶段调用 process.nextTick(),传递给 process.nextTick() 的所有回调都将在事件循环继续之前得到解决。
我的理解是,process.nextTick
的设计是为了能够在事件循环任何阶段开始前执行,比如下面代码
js// 这是一轮事件循环
setTimeout(() => {
console.log("timer1");
})
setImmediate(() => {
console.log("check1");
})
Promise.resolve().then(() => {
console.log("promise1");
})
process.nextTick(() => {
console.log("nextTick1");
})
fs.readFile("./index.js", () => {
// 这里是新的一轮事件循环
console.log("poll1");
setImmediate(() => {
console.log("check2");
})
setTimeout(() => {
console.log("timer2");
})
process.nextTick(() => {
console.log("nextTick2");
})
})
// 输出结果
/** nextTick作为微任务,优先级比Promise还高,所以每次事件循环都是nextTick先执行 **/
// nextTick1
// promise1
// timer1
// check1
/** 这是新的一轮事件循环,nextTick也是在check之前就执行了,印证了官网的说法 **/
// poll1
// nextTick2
// check2
// timer2
在 Node.js
中,宏任务和微任务的执行顺序依然遵循,先清空微任务,再执行一个宏任务,再清空微任务这种执行顺序
小试牛刀
jsconst fs = require("fs");
const start = Date.now();
setTimeout(() => {
console.log("timer1");
}, 200);
new Promise((resolve) => {
fs.readFile("./index.js", () => {
console.log("poll1");
while (Date.now() - start < 500);
setImmediate(() => {
console.log("immediate1");
});
process.nextTick(() => {
console.log("nextTick1");
Promise.resolve().then(() => {
console.log("promise1");
});
});
});
setTimeout(() => {
console.log("timer2");
resolve();
}, 100);
}).then(() => {
console.log("promise2");
});
// 输出结果
/** fs.readFile 完毕 开始一轮事件循环 **/
// poll1
/** nextTick 作为微任务,又有最高优先级最先执行 **/
// nextTick1
/** 推入微任务队列的Promise 立即执行 **/
// promise1
/** 到了宏任务的check阶段 **/
// immediate1
/** 到了宏任务的timer阶段,因为这个延迟时间更少,所以先执行 **/
// timer2
/** resolve promise之后,执行微任务 **/
// promise2
/** 最后执行timer1 其实当前还是在一轮事件循环的timer阶段 **/
// timer1
process.nextTick
优先级最高本文作者:pepedd864
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!