首先Event Loop(事件循环)是指计算机系统的一种运行机制。
它是浏览器或者Node用来解决JavaScript单线程运行时不会阻塞的一种机制,我们经常都会使用异步,而这种机制就是异步的原理。
先来说一下浏览器的Event Loop
JavaScript代码的执行过程中,除了依靠函数调用栈来确定函数的执行顺序外,还会依靠task queue(任务队列)(先进先出)来让另外一些代码的执行
Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行(代码从上到下的执行),异步任务会在异步任务有了结果后,将对应的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
任务队列又分为task(宏任务)与microtask(微任务)。
浏览器的异步实现:
1.宏观:浏览器多线程
2.微观:Event Loop
task(普通任务,宏任务):script(整体代码), setTimeout/ setInterval, setImmediate, I/O, UI rendering。
microtask(微任务):Promise, Object.observe(监听对象变化的方法), MutationObserver(监听Dom结构变化的API), postMessage(window对象之间进行通信的方法)
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始(全局任务也是宏任务)。之后全局进入函数调用栈。直到调用栈清空(宏任务执行完毕),然后执行所有的microtask(微任务)。当所有可执行的microtask(微任务)执行完后。循环再次从task(宏任务)开始,执行完毕宏任务之后,再执行所有的microtask(微任务),这样一直循环下去。 如下图
requestAnimationFrame处于渲染阶段,不属于任务队列
下面来一段代码示例来按上述事件循环顺序来走一下
console.log("start");
setTimeout(() => {
console.log("setTimeout");
new Promise(resolve => {
console.log("promise inner1");
resolve();
}).then(() => {
console.log("promise then1");
});
}, 0);
new Promise(resolve => {
console.log("promise inner2");
resolve();
}).then(() => {
console.log("promise then2");
});
// start->promise inner2->promise then2->setTimeout->promise inner1->promise then1
然后再来一个稍微复杂点的例子
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
return Promise.resolve().then(_ => {
console.log("async2 promise");
});
}
console.log("start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
// start->async1 start->promise1 ->async2 promise->promise2->async1 end->setTimeout
这样顺着这两个例子来推一推循环顺序,就可以基本理解了。
下面我们再说一下Node.js的Event Loop
node分三层构成
1.node-core
2.绑定
3.libuv + V8引擎
Node中的Event Loop是基于libuv实现的,而libuv是 Node 的新跨平台抽象层,libuv使用异步,事件驱动的编程方式,核心是提供I/O的事件循环和异步回调。libuv的API包含有时间,非阻塞的网络,异步文件操作,子进程等。 Event Loop就是在libuv中实现的。
Node的Event loop一共分为6个阶段,每个细节具体如下:
- timers:执行timer(定时器)的回调
- pending callbacks:系统操作的回调(待定回调执行延迟到下一个循环迭代的 I/O 回调。)
- idle,pepare:内部使用
- poll:等待新的I/O(输入/输出)事件(轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和setImmediate()调度的之外),其余情况 node 将在适当的时候在此阻塞。)
- check:执行setImmediate回调(检测)
- close callbacks:内部使用【关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)】
每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。