EventLoop-事件循环

149 阅读6分钟

无论是开发还是面试过程时,我们都会与事件循环打交道。很多时候都是以“javascript的事件循环是什么?出现,会让我们觉得javascript自带事件循环。但是在js作者Brendan Eich的《javascript二十年》中,根本就没有事件循环。故我们所说的事件循环可以理解为javascript所在的环境所提供的事件循环。比如,JavaScript在浏览器中,javascript就必须遵守浏览器的事件处理模型,在node.js中就要遵守node的事件处理模型。

事件模型定义

whatwg中对事件循环的定义是

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each has an associated event loop, which is unique to that agent。

根据标准中对事件循环的定义描述,我们可以发现事件循环本质上 是 user agent (如浏览器端) ⽤于协调⽤户交互(⿏标、键盘)、脚 本(如 JavaScript)、渲染(如 HTML DOM、CSS 样式)、⽹络 等⾏为的⼀个机制。

了解到这个定义之后,我们就能够清楚的明⽩,与其说是 JavaScript 提供了事件循环,不如说是嵌⼊ JavaScript 的 user agent 需要通过事件循环来与多种事件源交互。

浏览器事件

在我们打开的网页中,无论是加载资源,还是调用浏览器的API,这些都是通过环境的事件循环来管理的。

image.png 在浏览器中事件同时触发时,肯定有个先来后到的排队问题。决定这些事件如何排队触发的机制,就是事件循环。 从开发者的角度看,主要是分成两个队列:

  1. 一个是javascript的外部队列,这个队列是浏览器协调各类事件的队列,标准文件中称之为Task Queue。就是常说的宏任务队列。
  2. 另一个是javascript内部的队列。主要JavaScript内部执行的任务队列,标准中称之为Microtask Queue。也就是常说的微任务队列。

值得注意的是,虽然为了好理解我们管这个叫队列 (Queue),但是本质上是有序集 合 (Set),因为传统的队列都是先进先出(FIFO)的,⽽这⾥的队列则不然,排到最 前⾯但是没有满⾜条件也是不会执⾏的(⽐如宏任务队列⾥只有⼀个 setTimeout 的定 时任务,但是时间还没有到,没有满⾜条件也不会把他出列来执⾏)

宏任务队列

宏任务队列,可以理解为宏观的任务,既整个运行环境处理事件的任务通道,我们常用的有:

  • DOM操作(页面渲染)
  • 用户交互(鼠标、键盘)
  • 网络请求(Ajax等)
  • History Api操作
  • 定时器(setTimeout等)

其他的如调用摄像头、音频、视频也是宏任务的一种。 值得注意的是虽然setTimeout是常用的api,但是实现计时,是浏览器完成,故也是宏任务的一种。 还有在传统开发中的script标签也是宏任务的一种,毕竟script也是html标签,而不是JavaScript的内置对象。

微任务

微任务队列,是JavaScript语言内部的队列,可以理解为,javascript是依赖于环境的,相较于环境的队列,js内部处理事件的队列,是微小的。 在HTML标准中,并没有明确规定这个队列的事件源,通常认为有下面几种:

  • Promise的成功(.then)与失败(.catch)
  • MutaionObserve(DOM监听)
  • Object.observe(已废弃)

处理模型

未命名文件.png

以浏览器为例,其事件循环模型如图所示:

其基本流程为:

  1. 执行完成一个宏任务,等宏任务内部执行完成。
  2. 执行宏任务过程中产生的微任务。
  3. 如果需要浏览器渲染,则执行浏览器渲染任务。
  4. 开启下一个宏任务。

这么说有点抽象,拿一个常见的面试题举例。

console.log('1 script start'); 
setTimeout(function() { 
        console.log('2 setTimeout'); },
    0); 
Promise.resolve().then(
    function() { 
        console.log('3 promise1'); 
    }).then(function() {
    console.log('4 promise2'); 
    }); 
console.log('5 script end')
// 1->5->3->4->2

对应的处理过程是:

  1. 执行script这个宏任务
  2. 执行console.log(输出 1 script start)
  3. 遇到setTimeout将其插入宏任务队列
  4. 遇到promise的then,将其加入微任务队列。
  5. 执行conso.log直接执行,(输出 5 script end),至此,宏任务执行完成。
  6. 执行宏任务产生的微任务(输出 3 promise1和4 promise2),宏任务彻底完成。
  7. 开启下一个宏任务,开始执行(输出2 setTimeout)

来个复杂点的

console.log('1 script start'); 
setTimeout(function() { 
    console.log('2 setTimeout'); 
    Promise.resolve().then(function() { 
        console.log('3 promise1');
    }); 
}, 0); 
    Promise.resolve().then(function() { 
        console.log('4 promise1'); 
    }).then(function() { 
        console.log('5 promise2'); 
}); 
console.log('6 script end')

对应的处理流程是:

  1. 执行宏任务script
  2. 输出console.log(输出1 script start)
  3. 遇到setTimet,将其加入宏任务队列
  4. 遇到promise的then,将其他加入微任务队列
  5. 输出console.log(输出6 script end),宏任务执行完成。
  6. 执行宏任务产生的微任务队列,输出(4 promise1和5 promise2),宏任务彻底执行完成。
  7. 开启下一个宏任务,输出(2 setTimeout),宏任务执行完成。
  8. 执行微任务(3 promise1)

重点是要理解,setTimeout和script标签一下,是属于浏览器的宏任务,一切就很明了了。并且,如果定时器有时间,它会在你规定的时间加入宏任务队列。且是在前一个宏任务完成,才会执行下一个宏任务。比如,定时器是300ms时间,这个只是300ms后,浏览器会将这个任务加入宏任务队列中,并不是,300ms后就会一定执行。

在其中加入渲染阶段:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <pre id="main"></pre>
  <script>
    const main = document.querySelector('#main');
    const callback = (i,fn)=>()=>{
      console.log(i);
      main.innerText+=fn(i);
    }
    let i =1;
    while (i++<500){
      setTimeout(callback(i,(i)=>`\n${i}<`))
    };
    console.log(i)
    while(i++<1000){
      Promise.resolve().then(callback(i,i=>`${i},`))
    }
    console.log(i)
    main.innerText+=`[end ${i}]\n`
  </script>
</body>
</html>

处理过程如下:

  1. 执行script任务,获得dom节点,声明callbakc,i,while,console.log等,都是宏任务的一部分,打印出(1001)。
  2. 执行完成后 main.innerText首先加上[end 1001],但是页面并没有渲染出来。
  3. 微任务队列:promise的.then()执行完成,会在innerText追加上很长的字符串。(502,...)
  4. 执行浏览器渲染任务,2和3添加到innerText上的内容同时渲染出来,该宏任务彻底执行完成。
  5. 执行setTimeout中添加宏任务,打印i并且追加到innerText的内容,
  6. 没有产生微任务,直接跳过
  7. 渲染到页面上。
  8. 回到5走宏任务队列。

只要跳出js视角,站在js所依赖的环境(浏览器/nodejs)的视角,明白事件循环是浏览器处理各种事件的触发(js也是事件的一种),就能明白宏任务。而微任务是js这个宏任务中处理异步的机制。在只有结合处理模型,事件循环还是很容易理解的。