开头总是不知道写些什么东西……
为什么要有事件循环
JavaScript
被设计的初愿是执行在浏览器上可以动态加载的脚本,以便进行 DOM
节点的操作,所以,如果它是像 Java
一样的多线程语言,那么对 DOM
的操作就是未知且不可预测的了。
所以:
JavaScript
是一门单线程语言。
单线程就意味着有丶事情做起来没那么理想化,比如读取一个文件的事件,你以为它读完了文件内容而进行下一步的操作,然而运行的时候却发现,并没有这个结果存在,最后才发现,oh!!原来这个操作是异步的。。对,没错,单线程由此引出了 同步
和 异步
的问题。只有将某些操作(如读取文件),作为异步执行,才能避免 阻塞
,使脚本执行时间更迅速,用户体验更好。
所谓的事件循环,就是
JavaScript
执行异步操作的过程
同步与异步,阻塞与非阻塞
有段时间,我认为
异步
和非阻塞
就是同一样事。其实不然。。
- 阻塞,代码从上之下依次执行,后续的代码必须等待前面的代码执行完毕
- 非阻塞,代码从上至下依次执行,后续代码的执行不需要等待前面代码执行完毕
- 同步,主动等待前面代码执行完毕
- 异步,通过轮询监听异步操作是否执行完毕
也就是说:
- 阻塞和非阻塞指的是调用者的状态,关注的是程序在等待调用结果时的状态
- 同步和异步指的是被调用者是如何通知的,关注的是消息通知机制
通俗点讲,阻塞和非阻塞是指代码执行的顺序,同步和异步是指在等待消息的形式
好吧,举个粗鄙点的例子解释一下:
我和室友明天要交作业了,但我俩都没做,然后借了份已经做了的作业回来抄。秉着友好相处的理念,我让室友先抄,然后这时候就出现了 同步
和 异步
, 阻塞
和 非阻塞
的问题了
- 室友在抄作业,我在他桌上啥也不干,看着他抄完我再抄,这是
阻塞
,同步
- 室友在抄作业,我在他桌上啥也不干,他抄完了会告诉我,这是
阻塞
,异步
- 室友在抄作业,我在我桌上看剧,一段时间看一次他抄完没,这是
非阻塞
,同步
- 室友在抄作业,我在我桌上看剧,他抄完了会告诉我,这是
非阻塞
,异步
浏览器下的 Event Loop 的顺序
借图:
首先讲一下异步任务有分宏任务(macro-task)和微任务(micro-task):
macro-task包括: setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task包括:process.nextTick, Promise, Object.observe, MutationObserver。
process.nextTick 的执行优先级大于 Promise
在 JavaScript
执行的时候,会产生一个执行栈,其次,还会有一个任务队列,这个队列就是用来存放等待执行的异步操作的回调。。
emmm,有点绕,是这样的,当执行栈中发现有异步操作,会交给 Web APIs 去执行,并将执行结果放在队列中,当执行栈中主线程也就是同步代码执行完毕,就先将此时的 micro-task
执行,然后会把任务队列中的一个 macro-task
入栈出栈,并把存在的 micro-task
都执行,执行完之后,再将下一个 macro-task
入栈出栈,执行此时的 micro-task
,如此反复………………便是 Event loop
官方点的说法就是:
- 执行完主执行线程中的任务。
- 取出micro-task中任务执行直到清空。
- 取出macro-task中一个任务执行。
- 取出micro-task中任务执行直到清空。
- 重复3和4。
Node 下的 Event Loop 的顺序
这是 Node 官方文档中给出的 Node 事件循环的流程。可以一眼看出的是,microTasks 操作在每个阶段都执行一遍。先解释一下几个关键的环节:
- timers:这个环节里面执行的是和定时器相关的函数,如
setTimeout
- I/O callbacks:这个环节里面执行的是和
IO
读写相关的操作,如fs.readFile
- poll:检查上面两步是否已经完成
- check:执行 Node 特有的
setImmediate
所以是:
在每个阶段开始前,会清空一次 microTasks
中的任务,注意!
是在每个阶段进行前,会清空一次
microTasks
中的任务
- 这就是浏览器和 Node 事件循环的不同
对比
看一段代码
|
|
浏览器下执行结果:
Node 下执行结果:
仔细对比一下有什么不同
总结
- 在浏览器中,每执行一个
macro-task
,就会将当前的micro-task
清空 - 在 Node 中,每一个阶段开始前,就会将当前的
micro-task
清空,这个阶段可能不止一个macro-task