JS的EventLoop事件循环
相信前端的小伙伴都或多或少的了解过EventLoop这个概念,但可能很多人并没有去深入学习过这个概念,我也一样。在这次失业找工作的过程中,面试官提出来一个问题是Promise和setTimeout的执行顺序问题,由于之前看到过相关问题所以知道是Promise先执行的,但是当面试官询问其原理时便一无所知了,所以在面试之后查阅了一些资料以及其他前辈的一些博客,对EventLoop有了一些基础的了解,纪录在此,用写给别人看的方式逼迫自己好好理解这个概念,争取做到知其然知其所以然。
翻了翻自己的笔记,找到了以前记录的EventLoop相关概念,翻出来发现完全看不懂了
任务队列中,在每一次事件循环中,macrotask只会提取一个执行,而microtask会一直提取,直到microsoft队列为空为止。
也就是说如果某个microtask任务被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。而事件循环每次只会入栈一个macrotask,主线程执行完成该任务后又会检查microtasks队列并完成里面的所有任务后再执行macrotask的任务。
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise, MutationObserver
所以在此将EventLoop的基础概念从头到尾理一遍:
单线程的JS
大家都知道JavaScript是单线程的,作为一个浏览器的脚本语言,其主要用途是与用户互动 及 操作 DOM,它的使用场景决定了它只能是单线程的,否则会带来复杂的同步问题,假定JavaScript有两个线程,一个线程在某个DOM节点上添加内容,而另一个线程却删除了这个节点,此时浏览器应该以哪个线程为准呢?所以JavaScript是单线程的,但是为了充分利用多核CPU的计算能力,HTML5提出了Web Worker,允许创建多个线程,但是创建的子线程完全受主线程控制,且不允许子线程操作DOM,所以这个新标准 并没有改变JavaScript是单线程的本质。
关于Web Worker在此不展开描述,只是说明单线程问题。
单线程的代码执行是同步的而且会阻塞,如果只有同步执行肯定是不可以的,大家也都知道JavaScript是有异步函数的,比如setTimeout,而本文要说的Event Loop就是为了解决异步代码和同步代码的执行问题的。
相关术语概念
栈
栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。
栈是只能在某一端插入和删除的特殊线性表。
队列
队列也是一种数据结构,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出。
调用栈
调用栈本质上还是一个栈,存放的是待执行的函数。
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
任务队列
简单来说就是一个回调函数的队列,在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。
宏任务
宏任务包含了解析 HTML、生成 DOM、执行主线程 JS 代码和其他事件如 页面加载、输入、网络事件、定时器事件等。从浏览器的角度,宏任务代表的是一些离散的独立的工作。
如:script全部代码、setTimeout、setInterval、setImmediate、I/O、UI Rendering、 requestAnimationFrame。
微任务
微任务则是为了完成一些更新应用程序状态的较小的任务,如处理 Promise 的回调和 DOM 的修改,以便让这些任务在浏览器重新渲染之前执行。微任务 应该以异步的方式尽快执行,所以它们的开销比宏任务要小,并且可以使我们在 UI 重新渲染之前执行,避免了不必要的 UI 渲染。
如:Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver
执行顺序
在文章开头引入的之前笔记写着:
任务队列中,在每一次事件循环中,macrotask只会提取一个执行,而microtask会一直提取,直到microsoft队列为空为止。
其实EventLoop至少需要一个宏任务队列和一个微任务队列。
微任务队列具有更高的优先级,即执行一个宏任务后,就会去执行整个微任务队列,如果此时有新的微任务加入也会被执行,直至微任务队列清空才会再去执行宏任务队列。
贴一张流程图(图源自网络):
贴一个相关文章给出的最多的代码示例吧
1 | console.log('script start') |
经过前面的相关描述大家应该能直到执行的具体顺序了。
步骤:
- 执行开始,整个
script脚本作为第一个宏任务进入调用栈开始执行。 - 输出
script start - 执行到
setTimeout会将其加入宏任务队列进行等待。 - 执行到
Promise会将其加入到微任务队列等待。 - 输出
script end - 此时第一个宏任务执行完毕了,不论宏任务队列是否清空,会直接开始执行微任务队列中的任务,从中拿出一个放入调用栈执行
- 执行
Promsie会输出promise 1,返回undefined,第9行的回调会将其加入到微任务队列,此时由于微任务队列并没有清空,会继续执行输出promise 2。 - 微任务队列清空,执行宏任务队列。
- 执行
setTimeout,输出setTimeout,宏任务队列清空。 - 程序执行完成。
所以上述代码的执行顺序应该是:
1 | script start |
扩展知识
在ES7中提出了Promise的语法糖async/await,其写法更加简便,也更加容易理解。
需要注意的是,async这个函数总是返回一个promise,不论你是否显式的返回了一个Promise。这也就说明async函数是一个异步函数,且由于是对promise的封装,所以其调用也会加入到微任务队列。
而await必须在async函数内部才能使用。
JS的EventLoop事件循环
