JavaScript事件循环探索
一直对js的事件循环不是很清晰,最近看了JavaScript忍者秘籍的第13章后,有了一些感悟,特此总结一下,分享给大家。
单线程
众所周知,JavaScript是单线程执行模型,同一时刻只能执行一个代码片段,一个任务开始后知道运行完成,不会被其他任务中断。当一个任务花费的时间很长的话,用户就会明显的感觉到卡顿。浏览器为了解决这个问题引入了事件循环的概念(Event Loop)。
事件循环
事件循环具有至少两个队列处理任务。任务分为两类,宏任务(macro-task)和微任务(micro-task)。
1.宏任务代表一个个离散、独立的工作单元,运行完之后,浏览器可以继续其他的调度。包括:创建文档对象,解析HTML,执行JavaScript,以及各种事件…… 2.微任务是更小的任务,主要用户更新应用程序的状态,必须在浏览器任务继续执行其他任务之前执行。微任务需要尽可能快地通过异步方式执行,同时不能产生全新的微任务。包括promise、回调函数、DOM发生变化……
仅包含宏任务
// 主线程JavaScript运行15ms btn1.addEventListener('click', function() {运行 8ms}, false); btn2.addEventListener('click', function() {运行 5ms}, false);
现在假设主线程运行15ms, 在第5ms单击btn1,在第12ms的时候单击btn2。基于单线程执行模型,单击按钮之后不会立即执行对应的处理函数,因为一个任务一旦开始就不会被另一个任务中断。因此,在主线程执行的15ms期间,按钮的单击处理函数放入队列。当主线程执行完成也就是15ms之后,程序开始处理微任务,因为当前不存在微任务,跳过此步骤,开始执行更新UI。
之后进入第二次循环,也就是开始执行btn1的处理函数,需要运行8ms,btn2处理函数在队列中等待。当btn1处理函数执行完之后,浏览器检查微任务是否存在和是否更新UI,删除任务队列里的btn1的处理函数。
最后进入第三次循环,开始执行btn2的处理函数,需要运行5ms,处理函数执行完之后,检查微任务和是否需要更新UI,删除任务队列里的btn2的处理函数,最终任务队列为空,循环结束。
同时含有宏任务和微任务
// 主线程JavaScript运行15ms btn1.addEventListener('click', function() { Promise.resolve().then(() => { 运行 4ms }); 运行 8ms }, false); btn2.addEventListener('click', function() {运行 5ms}, false);
本例中在btn1的事件处理函数里增加了一个立即兑现的Promise,需要运行4ms。
现在代码的执行顺序为:
- 主线程执行15ms,在5ms和12ms的时候分别将处理函数放入任务队列,更新UI。
- 15m后处理btn1事件处理函数,发现Promise,放入微任务队列,btn1事件处理函数继续执行8ms,检查微任务队列发现有Promise回调函数,然后开始执行Promise回调函数,运行4ms,继续检查微任务队列,如果为空,检查是否需要更新UI,进入下一轮循环。
- 处理btn2的事件处理函数……
计时器
// 主线程JavaScript运行18ms setTimeout(function() { 运行6ms; }, 10); setInterval(function() { 运行8ms; }, 10); btn1.addEventListener('click', function() {运行 10ms}, false);