概览
我们经常会听到引擎和runtime,它们的区别是什么呢?
- 引擎:解释并编译代码,让它变成能交给机器运行的代码(runnable commands)。
- runtime:就是运行环境,它提供一些对外接口供Js调用,以跟外界打交道,比如,浏览器环境、Node.js环境。不同的runtime,会提供不同的接口,比如,在 Node.js 环境中,我们可以通过
require
来引入模块;而在浏览器中,我们有window
、 DOM。
Js引擎是单线程的,如上图中,它负责维护任务队列,并通过 Event Loop 的机制,按顺序把任务放入栈中执行。而图中的异步处理模块,就是 runtime 提供的,拥有和Js引擎互不干扰的线程。接下来,我们会细说图中的:栈和任务队列。
栈
现在,我们要运行下面这段代码:
function bar() {
console.log(1);
}
function foo() {
console.log(2);
far();
}
setTimeout(() => {
console.log(3)
});
foo();
它在栈中的入栈、出栈过程,如下图:
任务队列
Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?
- 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
- 微任务:process.nextTick, Promise, Object.observer, MutationObserver.
浏览器的 Event Loop
浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv。 区别较大,分开讲。
我们上面讲到,当stack空的时候,就会从任务队列中,取任务来执行。浏览器这边,共分3步:
- 取一个宏任务来执行。执行完毕后,下一步。
- 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。
- 更新UI渲染。
Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。
从执行步骤来看,我们发现微任务,受到了特殊待遇!我们代码开始执行都是从script(全局任务)开始,所以,一旦我们的全局任务(属于宏任务)执行完,就马上执行完整个微任务队列。看个例子:
console.log('script start');
// 微任务
Promise.resolve().then(() => {
console.log('p 1');
});
// 宏任务
setTimeout(() => {
console.log('setTimeout');
}, 0);
var s = new Date();
while(new Date() - s < 50); // 阻塞50ms
// 微任务
Promise.resolve().then(() => {
console.log('p 2');
});
console.log('script ent');
/*** output ***/
// one macro task
script start
script ent
// all micro tasks
p 1
p 2
// one macro task again
setTimeout
上面之所以加50ms的阻塞,是因为 setTimeout
的 delayTime 最少是 4ms. 为了避免认为 setTimeout
是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。
NodeJs 的 Event Loop
NodeJs 的运行是这样的:
- 初始化 Event Loop
- 执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。
- 执行主代码中出现的所有微任务:先执行完所有nextTick(),然后在执行其它所有微任务。
- 开始 Event Loop
NodeJs 的 Event Loop 分6个阶段执行:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
以上的6个阶段,具体处理的任务如下:
- timers: 这个阶段执行
setTimeout()
和setInterval()
设定的回调。 - pending callbacks: 上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。
- idle, prepare: 仅内部使用。
- poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段
- check: 执行
setImmediate()
设定的回调。 - close callbacks: 执行比如
socket.on('close', ...)
的回调。
每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。
Links
- Event loops
- NodeJs 的 Event Loop 官方文档
- 并发模型与事件循环
- Philip Roberts: Help, I’m stuck in an event-loop.
- Promise的队列与setTimeout的队列有何关联?
- JavaScript:彻底理解同步、异步和事件循环(Event Loop)
- 从event loop规范探究javaScript异步及浏览器更新渲染时机
- JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志
- Tasks, microtasks, queues and schedules
- WindowOrWorkerGlobalScope.setTimeout()
- What is the difference between JavaScript Engine and JavaScript Runtime Environment
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。