Event loop

为什么要了解Event loop?
理解Event loop,对于浏览器(或者nodejs)处理事件的过程会有更透彻的理解,使用promise,nextTick, setImmediate,setTimeout等会更清晰。本文主要是基于浏览器端来理解的。

<small>有部分术语还是采用英文,看上去感觉比中文好理解</small>

参考自:
Tasks, microtasks, queues and schedules
What the heck is the event loop anyway?
Concurrency model and Event Loop
Macrotasks and Microtasks

  • 并发模型(Concurrency model)

首先我们知道,js是单线程的,要么执行脚本要么进行浏览器渲染。
执行脚本时,通常是这样子的:

runtime.png

举个例子(直接从mdn搬过来):

function f(b){ 
  var a = 12;
  return a+b+35;
}

function g(x){ 
  var m = 4; 
  return f(m*x);
}

g(21);
  1. 当执行 g(21)的时候,创建了第一个frame,包含g(x)和它的局部变量,压入栈(stack);
  2. 当执行到f(m*x)的时候,创建了第二个frame,包含f(x)和它的局部变量,压入栈;
  3. 等到f(x)执行完毕,第二个frame出栈;
  4. 等到g(x)执行完毕,第一个frame出栈;
  • “线程” 与 Event loop

每个“线程”有自己的event loop,比如说每个web worker都维护了自己的event loop,可以分开来工作,彼此通过postMessage通信。如下图:

webworker.png

主线程和web worker的event loop的区别在于,主线程每次task完成后会进行视图更新,但是worker和dom无关,就没有这一步了.

  • Task

如上图,task就是在message queue里的message了(我是这么理解的),一次event loop里面可能会有多个task,task有自己的task source,比如说setTimeout来自于timer task source,又或者和用户交互相关的来自user interaction task source。

浏览器是可以选择先执行哪个task source 的。规范如下:

For example, a user agent could have one task queue for mouse and key events (the user interaction task source), and another for everything else. The user agent could then give keyboard and mouse events preference over other tasks three quarters of the time, keeping the interface responsive but not starving other task queues, and never processing events from any one task source out of order.

有点晕吧,至少我们弄清楚,在js运行过程中,起码有两个东西:

  • task队列

最简单的就是每次event loop查看task队列,找到最老的task,压入栈,执行之。等到task执行完了,就进行一次视图渲染。周而复始,直到所有task队列都执行完毕。

等等,这样的话,那setTimeout又是怎么做到的?还有promise,要是都是顺序执行的话,这些是怎么做到异步的?

  • Microtask 和 Macrotask

实际上我看到有些文章提到Macrotask,但也有些文章直接把Macrotask当成task,可以区分一下。

接着上文说的,promise是怎么做到异步的呢?
promise并不是task,它属于Microtask,什么是Microtask呢?

Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.

感觉可以叫它微任务~ 跟在task末尾执行,像个小跟班。

实际上来说,Microtask并非每次都在task末尾才执行,如果一个函数执行完毕后,栈暂时空掉了,那么Microtask也会执行(晕了对不对,后面上代码详述~)

那么什么是Macrotask呢?setTimeout就是Macrotask,还有setImmediate。
对比着Microtask来,他们的区别就在于执行的时机,Macrotask在一次task执行完了,然后浏览器进行渲染,然后才执行Macrotask。

就叫宏任务吧~

所以它俩的区别就在于Microtask会影响IO回调,要是不断增加Microtask的话,就一直无法渲染视图了,看上去就会卡顿。但是Macrotask就没有这种危险。

按我的理解,可以把Macrotask直接当成task来看。

总结下,现在在js运行中,起码有三个东西:

  • task队列

  • Microtask队列

  • 用代码来感受

先来简单的:

  console.log('script start');

  setTimeout(function() { 
    console.log('setTimeout');
  }, 0);

  Promise.resolve().then(function() {   
    console.log('promise1');
  }).then(function() { 
    console.log('promise2');
  });
  
  console.log('script end');

结果应当如下:

script start
script end
promise1
promise2
setTimeout

假设我们维护三个队列,分别是:tasks, stacks和Microtasks,上面的代码执行情况应该如下:

  1. 执行脚本,把这段脚本压入tasks,此时

tasks: [script],stacks: [],Microtasks: [];

  1. 执行脚本,打印script start,此时

tasks:[script]
stacks: [script]
Microtasks: [ ];

  1. 遇到setTimeout,由上文可知,setTimeout可当作一个task,所以此时

tasks: [script, setTimeout callback]
stacks: [script]
Microtasks: [ ];

  1. 遇到Promise, Promise是Microtask,所以此时

tasks: [script, setTimeout callback]
stacks: [script]
Microtasks: [Promise then];

  1. 执行脚本,打印script end,此时:

tasks: [script, setTimeout callback]
stacks: []
Microtasks: [Promise then];

  1. 在一个task执行完之后执行Microtask,此时:

tasks: [setTimeout callback]
stacks: [Promise callback]
Microtasks: [Promise then];

  1. 打印promise1, .Promise then后面还有一个Promise,压入Microtasks:

tasks: [setTimeout callback]
stacks: [ ]
Microtasks: [PromiseThen]

  1. 执行Promise 2号,此时:

tasks: [setTimeout callback]
stacks: [Promise callback]
Microtasks: [PromiseThen];

  1. 打印出promise 2,此时:

tasks: [setTimeout callback]
stacks: []
Microtasks: [];

  1. 进行浏览器渲染,然后执行setTimeout脚本,此时:

tasks: [setTimeout callback]
stacks: [setTimeout callback]
Microtasks: [];

  1. 打印出setTimeout,此时:

tasks: []
stacks: []
Microtasks: [];

  • 为什么不一样

如果你用的不是chrome浏览器的话,表现很可能就会不一样,甚至可能一段代码两次执行结果都不一样(比如说在ios 8的safri)
对的,因为这是规范== 浏览器会有差异。前途是光明的,而道路则是曲折的~

  • 继续感受代码

html如下:
<div class="outer">
<div class="inner"></div>
</div>
js如下:
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

  // Let's listen for attribute changes on the
  // outer element
  new MutationObserver(function() { 
    console.log('mutate');
  }).observe(outer, { 
    attributes: true
  });

  // Here's a click listener…
  function onClick() { 
    console.log('click'); 

    setTimeout(function() { 
      console.log('timeout'); 
    }, 0); 

    Promise.resolve().then(function() { 
      console.log('promise'); 
    }); 

    outer.setAttribute('data-random', Math.random());
  }

  // …which we'll attach to both elements
  inner.addEventListener('click', onClick);
  outer.addEventListener('click', onClick);

出来结果应该是:

click
promise
mutate
click
promise
mutate
timeout
timeout

  • why

参考event loop的处理模型,整个处理如下:

  1. 将脚本压入tasks,运行脚本,压入stacks。执行完毕清空;
  2. 用户点击,触发点击,将 onClick压入task,执行onClick,即压入stacks;
  3. 打印click
  4. 将setTimeout压入task;
  5. 将Promise压入Microtask;
  6. 设置outer属性,将MutationObserver压入Microtask;
  7. 一次onClick执行完毕,stacks清空了,这时候虽然后面还有冒泡触发,但是会先执行Microtask(真是见缝插针),Microtasks顺序执行,打印promisemutate
  8. 事件冒泡,执行outer的onClick,执行过程差不多。等到冒泡执行完毕,情况如下:

tasks: [setTimeout, setTimeout]
stacks: [ ]
Microtasks:[ ]

  1. 执行浏览器渲染
  2. 执行tasks,依次打印出setTimeout setTimeout,搞定。
  • 进阶版

前面我们使用交互来触发click 事件,如果我们在刚刚代码后面加inner.click(),整个过程就变化了,不妨思考。答案可以在Tasks, microtasks, queues and schedules找到

其实文章代码基本上挪用了Tasks, microtasks, queues and schedules,加了一些自己的理解,欢迎指正。

推荐阅读更多精彩内容