Event Loop
一. JavaScript单线程
JavaScript单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
那么,由于js单线程的特点,如果JS发起了一个异步IO请求,在等待结果返回的这个时间段,后面的代码都会被阻塞。 我们知道JS主线程和渲染进程是相互阻塞的,因此这就会造成浏览器假死。 如何解决这个问题? 一个有效的办法就是我们这节要讲的事件循环Event Loop。
二. 什么是事件循环?
js中所有任务分为两种:同步任务synchronous、异步任务asynchronous
// 执行步骤如下: // 1.执行 log(1),输出 1; // 2.遇到 setTimeout,将回调的代码 log(2)添加到宏任务中等待执行; // 3.执行 console.log(3),将 then 中的 log(4)添加到微任务中; // 4.执行 log(5),输出 5; // 5.遇到 setTimeout,将回调的代码 log(6, 7)添加到宏任务中; // 6.宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,存在一个微任务 log(4)(在步骤 3 中添加的),执行输出 4; // 7.取出下一个宏任务 log(2)执行,输出 2; // 8.宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,不存在; // 9.取出下一个宏任务执行,执行 log(6),将 then 中的 log(7)添加到微任务中; // 10.宏任务执行完毕,存在一个微任务 log(7)(在步骤 9 中添加的),执行输出 7; // 因此,最终的输出顺序为:1, 3, 5, 4, 2, 6, 7; // 参考 https://juejin.cn/post/6844904035770695693 四. 调用栈call stack、消息队列Message queue、微任务队列 MicroTask queue 1.js在函数之前执行之前,会创建执行上下文,函数在执行时,会被压入调用栈 2.遇到异步任务,如果是宏任务,会将宏任务的回调函数,放入消息队列,等待执行 3.如果是微任务,会将微任务的回调函数,放入到微任务队列,等待执行 4.等调用栈清空,微任务队列中的任务,会依次压入调用栈执行并弹出 ,微任务队列清空后,消息队列中的任务,会依次压入调用栈执行并弹出 参考:https://www.bilibili.com/video/BV1kf4y1U7Ln?from=search&seid=2734638953578049157 五.Node.js中的事件循环 1.node.js中的事件循环的特点:
- 同步任务指的是:立即执行的任务,同步任务一般会直接进入到主线程中执行;
- 异步任务指的是:异步执行的任务,比如 ajax 网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列的机制(先进先出的机制)来进行协调
- 异步任务又分为:宏任务和微任务
- 宏任务:
- js(整体代码)
- I/O、UI 渲染
- MessageChannel、postMessage
- setImmediate(Node.js 环境)
- setTimeout、setInterval
- requestAnimationFrame 属于 GUI 引擎,发生在渲染过程的重绘重排部分,在 UI 渲染之前执行
- 微任务:
- process.nextTick(Node.js 环境)
- MutaionObserver(浏览器环境)
- Promise的回调
- js运行机制:
- 从宏任务的头部取出一个任务执行;
- 执行过程中若遇到微任务则将其添加到微任务的队列中;
- 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则挨个儿出去执行,直到执行完毕;
- GUI 渲染;
- 回到步骤 1,直到宏任务执行完毕;
// 执行步骤如下: // 1.执行 log(1),输出 1; // 2.遇到 setTimeout,将回调的代码 log(2)添加到宏任务中等待执行; // 3.执行 console.log(3),将 then 中的 log(4)添加到微任务中; // 4.执行 log(5),输出 5; // 5.遇到 setTimeout,将回调的代码 log(6, 7)添加到宏任务中; // 6.宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,存在一个微任务 log(4)(在步骤 3 中添加的),执行输出 4; // 7.取出下一个宏任务 log(2)执行,输出 2; // 8.宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,不存在; // 9.取出下一个宏任务执行,执行 log(6),将 then 中的 log(7)添加到微任务中; // 10.宏任务执行完毕,存在一个微任务 log(7)(在步骤 9 中添加的),执行输出 7; // 因此,最终的输出顺序为:1, 3, 5, 4, 2, 6, 7; // 参考 https://juejin.cn/post/6844904035770695693 四. 调用栈call stack、消息队列Message queue、微任务队列 MicroTask queue 1.js在函数之前执行之前,会创建执行上下文,函数在执行时,会被压入调用栈 2.遇到异步任务,如果是宏任务,会将宏任务的回调函数,放入消息队列,等待执行 3.如果是微任务,会将微任务的回调函数,放入到微任务队列,等待执行 4.等调用栈清空,微任务队列中的任务,会依次压入调用栈执行并弹出 ,微任务队列清空后,消息队列中的任务,会依次压入调用栈执行并弹出 参考:https://www.bilibili.com/video/BV1kf4y1U7Ln?from=search&seid=2734638953578049157 五.Node.js中的事件循环 1.node.js中的事件循环的特点:
- Node.js 通过 libuv 来处理与操作系统的交互,并且因此具备了异步、非阻塞、事件驱动的能力。
- Node.js 实际上是 Javascript 执行线程的单线程,真正的的 I/O 操作,底层 API 调用都是通过多线程执行的。
- CPU 密集型的任务是 Node.js 的软肋。
- 参考:https://developer.aliyun.com/article/3203
- node.js中的Event Loop的单次循环是分阶段进行的。每个阶段运行完所有该阶段的回调函数或回调次数达到了次数限制,才会进入下一个阶段或指定的阶段,直到运行完最后一个阶段,进入下一个循环。
- 除了Poll阶段,node会在每个阶段,将该阶段对应的所有宏任务都依次执行完,然后执行微任务队列中的所有任务。(注意:node.js 11.0及以后的版本中,Goole为了向浏览器靠齐,将这一行为改成与浏览器一致,即每个 Macrotask(setTimeout,setInterval和setImmediate) 执行完后,就去执行 Microtask 了)
- 定时器检测阶段(timers): 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
- I/O事件回调阶段(I/O callbacks): 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
- 闲置阶段(idle, prepare): 这个阶段仅在内部使用,可以不必理会
- 轮询阶段(poll): 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- 检查阶段(check): setImmediate()的回调会在这个阶段执行。
- 关闭事件回调阶段(close callbacks): 例如socket.on('close', ...)这种close事件的回调
- setTimeout()和setInterval()的回调函数
- setImmediate()的回调函数
- 用于关闭请求的回调函数,比如socket.on('close', ...)