JavaScript事件循环探索

MarkArch 2019-06-28

一直对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。

现在代码的执行顺序为:

  1. 主线程执行15ms,在5ms和12ms的时候分别将处理函数放入任务队列,更新UI。
  2. 15m后处理btn1事件处理函数,发现Promise,放入微任务队列,btn1事件处理函数继续执行8ms,检查微任务队列发现有Promise回调函数,然后开始执行Promise回调函数,运行4ms,继续检查微任务队列,如果为空,检查是否需要更新UI,进入下一轮循环。
  3. 处理btn2的事件处理函数……

计时器

// 主线程JavaScript运行18ms
setTimeout(function() {
    运行6ms;
}, 10);
setInterval(function() {
    运行8ms;
}, 10);
btn1.addEventListener('click', function() {运行 10ms}, false);

相关推荐