同步和异步、阻塞和非阻塞
同步和异步关注的是消息通信机制,描述的是一种行为方式
- 同步:发出调用后,在没有得到结果之前,该调用不返回。当调用返回时得到返回值。调用者主动等待这个调用结果
- 异步:发出调用后,立即返回。但调用方通常不会立即得到结果。调用者需要等待被调用者通知才能得到调用结果
阻塞非阻塞关注的是程序在等待调用结果时的状态,描述的是一种状态
- 阻塞:调用结果返回之前,线程被挂起,只有得到结果之后才会激活
- 非阻塞:调用后不等待结果,该调用不阻塞当前进程
EventLoop
JavaScript
是一门单线程的语言,起初只是为了实现简单的功能而设计的脚本语言。单线程存在着任务执行阻塞的问题,遇到耗时的操作时,容易导致页面长时间无响应。
EventLoop
(事件循环)是让 JavaScript
做到即是单线程运行,又不会阻塞的一种机制。是 JavaScript
并发模型的基础,用于协调各类事件、交互、脚本执行、UI渲染、网络请求等操作
EventLoop
由三个部分组成,函数调用栈、宏任务队列(macro-task-queue)和微任务队列(micro-task-queue)
函数调用栈
当引擎遇到 JS
代码时,会产生全局上下文,并将其压入调用栈中,后面每当遇到函数调用都会往栈中压入新的函数上下文,执行完栈顶内容后,弹出对应的上下文
宏任务队列
通过队列存放被注册的宏任务
- 常见的宏任务(MacroTask):
script
中的代码、setTimeout
、setInterval
、setImmediate(ie10、node)
、postMessage(MessageChannel)
、I/O
、UI渲染
、网络请求
、History API
requestAnimationFrame
不是宏任务!它会在微任务结束后,下一个EventLoop
开始前去执行
微任务队列
通过队列存放被注册的微任务
- 常见的微任务(MicroTask):
Promise callback
、MutationObserver
、Object.observe
循环过程
一个宏任务一队微任务
- 调用栈选择最先进入队列的
MacroTask
,执行过程如果产生新的MacroTask
或MicroTask
分别入队 - 执行完毕第一个
MacroTask
,检查当前的MicroTask
队列,执行至清空MicroTaskQueue
- 浏览器检查更新渲染(
render
),每次循环都可能会检查更新渲染 - 重复 1-3,直到所有队列都为空
例子
1 | let logs = [] |
遇到
setTimeout
将回调入队macro-task-queue
1
2
3
4
5
6
7
8
9setTimeout(() => {
log('timer - 1')
new Promise((resolve) => {
log('sync - 1')
resolve()
}).then(() => {
log('then - 1')
})
})接着执行
new Promise
中的同步代码,又遇到setTimeout
入队
1 | new Promise((resolve) => { |
- 执行完成同步代码后,入队
then
微任务micro-task-queue
1 | .then(() => { |
- 此时同步代码已经执行完毕出栈,依次执行
micro-task-queue
中的任务
micro-task-queue
清空,执行macro-task-queue
队首setTimeout1
- 执行完
new Promise
中的同步代码,then2
入队micro-task-queue
1 | log('timer - 1') |
- 执行完
setTimeout1
后,接着清空micro-task-queue
1 | .then(() => { |
- 清空后,再次取出
macro-task-queue
队首,同样的操作,执行遇到microTask
后又入队then3
1 | setTimeout(() => { |
- 执行完最后的
then3
后,数组收集到的数据应该为
1 | [ |
NodeJS 中的 EventLoop
- times:执行
setTimeout()
和setInterval()
中定义的回调函数 - pending callbacks:处理网络I/O或文件I/O中的错误的回调(比较少见)
- idle, prepare:仅系统内部使用(忽略)
- poll:执行 I/O 回调,处理轮询队列中的事件,同时检查定时器是否过期
- poll 阶段处理的回调中,如果既派发了 setImmediate、又派发了 setTimeout,一定是先执行 setImmediate,再执行 setTimeout。
- check:执行
setImmediate()
中定义的回调函数 - close callbacks:处理“关闭”的回调函数,
socket.on('close', ...)
任务队列
宏任务队列
Timers Queue
IO Callbacks Queue
Check Queue
Close Callbacks Queue
微任务队列
Next Tick Queue
:放置process.nextTick(callback)
Other Micro Queue
:放置其他微任务
执行顺序
一队一队执行!
- 执行
script
同步代码 - 执行
microTaskQueue
,优先清空Next Tick Queue
中的任务,随后才会清空其它微任务- 先执行
Next Tick Queue
,所有callbacks
会被依次调用 - 再执行
Other Mico Queue
- 先执行
- 执行
macroTaskQueue
- 每个阶段的宏任务执行完后执行微任务
需要特别注意的是
Node11开始,timers 阶段的setTimeout、setInterval等函数派发的任务、包括 setImmediate 派发的任务,都被修改为:一旦执行完当前阶段的一个任务,就立刻执行微任务队列。
1 | setTimeout(() => { |