CodingMeUp / AboutFE

知识归纳、内容都在issue里
74 stars 14 forks source link

39、EventLoop 事件循环,宏/微任务 #40

Open CodingMeUp opened 4 years ago

CodingMeUp commented 4 years ago

定义

event loop翻译出来就是事件循环,可以理解为实现异步的一种方式 一个event loop有一个或者多个task队列

task任务源:

有两种event loops,一种在浏览器上下文,一种在workers中。

浏览器上下文 browsing contexts是一个将 Document 对象呈现给用户的环境。在一个 Web 浏览器内,一个标签页或窗口常包含一个浏览上下文,如一个 iframe 或一个 frameset 内的若干 frame。

Worker的event loop相对简单一些,一个worker对应一个event loop,worker进程模型管理event loop的生命周期

event loop的处理过程(Processing model

一个event loop只要存在,就会不断执行下边的步骤:

  1. 在tasks队列中选择最老的一个task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到下边的microtasks步骤。

  2. 将上边选择的task设置为正在运行的task。

  3. Run: 运行被选择的task。

  4. 将event loop的currently running task变为null。

  5. 从task队列里移除前边运行的task。

  6. Microtasks: 执行microtasks任务检查点。(也就是执行microtasks队列里的任务)

  7. 更新渲染(Update the rendering)规范允许浏览器自己选择是否更新视图。也就是说可能不是每轮事件循环都去更新视图,只在有必要的时候才更新视图

    • 在一轮event loop中多次修改同一dom,只有最后一次会进行绘制。
    • 渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms只是估算并不准确)修改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
    • 如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame。
  8. 如果这是一个worker event loop,但是没有任务在task队列中,并且WorkerGlobalScope对象的closing标识为true,则销毁event loop,中止这些步骤,然后进行定义在Web workers章节的run a worker。

  9. 返回到第一步。

概括说来

  1. event loop会不断循环的去取tasks队列的中最老的一个任务推入栈中执行,并在当次循环里依次执行并清空microtask队列里的任务。
  2. 执行完microtask队列里的任务,有可能会渲染更新。(浏览器很聪明,在一帧以内的多次dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ的频率更新视图)

基本原理

首先对于异步事件,我们在执行到这行代码的时候会进行一个注册,将你要在未来某个时间段要执行的函数注册一下,放在Event table中。这个Event table中可以有很多事件,比如你一次发了好多ajax请求,那么他们就全部注册了。在未来的时间到了,就会把注册的事件放入Event queue(任务队列)这个任务队列就是马上要执行的内容。

任务队列什么时候可以执行?在主线程的call stack为空的时候(会在Event queue检查一下哪些是宏任务哪些是微任务,然后执行所有的微任务,然后执行一个宏任务,之后再次执行所有的微任务。也就是说在主线程任务执行完毕后会把任务队列中的微任务全部执行,然后再执行一个宏任务,这个宏任务执行完再次检查队列内部的微任务,有就全部执行没有就再执行一个宏任务。 ),会把任务队列的第一个事件放入call stack中执行,这里面涉及一个queue(队列)的特点就是先进先出。在注册后先放入Event queue的事件就会更早的离开Event queue进入主线程执行。

在这个概念理解清楚之后,我大约明白一些了,可是我就是一行真正的脱离了定时器、事件、ajax的异步的代码也写不出来,我一直在想如何实现消息通知异步的消息通知?观察者模式吗?那也没办法模拟出来,假如我们要做一个定时提醒的需求,这个时候后台的任务分配中选择了几天来制作定时任务。这个时候我理解了。java的定时提醒是要开一个新的线程去不断轮巡时间,可以设置的间隔但是间隔越小,越消耗机器的性能。后来查阅资料了解,JS是单线程,但是浏览器不是,只是执行JS代码的引擎是个单线程,所以JS的代码没办法开启多个线程,但是浏览器还有定时器线程、事件触发线程、异步http请求线程、GUI线程。 所以在执行定时器、事件、ajax这些异步事件的时候是另外三个线程在执行代码,并不是JS引擎在做事情,在这些线程达到某一特定事件把任务放入JS引擎的线程中,同时GUI线程(渲染界面HTMl的线程)与JS线程是互斥的,在JS引擎执行时GUI线程会被冻结、挂起。

最后最后JS是单线程但是浏览器是多线程。你的异步任务是浏览器开启对应的线程来执行的,最后放入JS引擎中进行执行。

宏任务 macrotask(setTimeout、setInterval(定时器类)) - 微任务跑完跑1个宏任务

Wiki 上面对‘宏’的定义是:宏(Macro), 是一种批处理的称谓,它根据一系列的预定义规则转换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式转换,这个转换过程被称为“宏展开(Macro Expansion)”。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。 你可以认为,宏就是用来生成代码的代码,它有能力进行一些句法解析和代码转换。宏大致可以分为两种: 文本替换和语法扩展

定时器

setTimeout(()=>{console.log(111)},500);

请问打印出111会在什么时候? 答案是500ms的时候打印

错!答案是500ms或者500ms以后的某个时间段。

遇见定时器后,会将定时器内的函数进行注册,也就是放入Event Table 。然后在xx ms后将Event Table内注册的函数放入 Event queue。若主线程(我也就一个线程)中的call stack(调用堆栈,也就是线程中函数的一个调用栈)为空就将Event queue按顺序的放入call stack中进行执行。如果call stack并不为空, Event queue内的事件并不会进入call stack中,也就不会执行。

微任务 microtask

在Promises/A+规范的Notes 3.1中提及了promise的then方法可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。所以promise在不同浏览器的差异正源于此,有的浏览器将then放入了macro-task队列,有的放入了micro-task 队列。在jake的博文Tasks, microtasks, queues and schedules中提及了一个讨论vague mailing list discussions,一个普遍的共识是promises属于microtasks队列。

microtasks检查点(microtask checkpoint)

event loop运行的第6步,执行了一个microtask checkpoint,看看规范如何描述microtask checkpoint:

当用户代理去执行一个microtask checkpoint,如果microtask checkpoint的flag(标识)为false,用户代理必须运行下面的步骤:

  1. 将microtask checkpoint的flag设为true。
  2. Microtask queue handling: 如果event loop的microtask队列为空,直接跳到第八步(Done)。
  3. 在microtask队列中选择最老的一个任务。
  4. 将上一步选择的任务设为event loop的currently running task。
  5. 运行选择的任务。
  6. 将event loop的currently running task变为null。
  7. 将前面运行的microtask从microtask队列中删除,然后返回到第二步(Microtask queue handling)。
  8. Done: 每一个environment settings object它们的 responsible event loop就是当前的event loop,会给environment settings object发一个 rejected promises 的通知。
  9. 清理IndexedDB的事务。
  10. 将microtask checkpoint的flag设为flase。

microtask checkpoint所做的就是执行microtask队列里的任务。什么时候会调用microtask checkpoint呢?

  1. 当上下文执行栈为空时,执行一个microtask checkpoint
  2. 在event loop的第六步(Microtasks: Perform a microtask checkpoint)执行checkpoint,也就是在运行task之后,更新渲染之前

完整异步过程

主线程类似一个加工厂,它只有一条流水线,待执行的任务就是流水线上的原料,只有前一个加工完,后一个才能进行。event loops就是把原料放上流水线的工人。只要已经放在流水线上的,它们会被依次处理,称为同步任务。一些待处理的原料,工人会按照它们的种类排序,在适当的时机放上流水线,这些称为异步任务

举个简单的例子,假设一个script标签的代码如下:

Promise.resolve().then(function promise1 () {
       console.log('promise1');
    })
setTimeout(function setTimeout1 (){
    console.log('setTimeout1')
    Promise.resolve().then(function  promise2 () {
       console.log('promise2');
    })
}, 0)

setTimeout(function setTimeout2 (){
   console.log('setTimeout2')
}, 0)
// promise1
// setTimeout1
// promise2
// setTimeout2

运行过程:

script里的代码被列为一个task,放入task队列。

循环1:

【task队列:script ;microtask队列:】

  1. 从task队列中取出script任务,推入栈中执行。
  2. promise1列为microtask,setTimeout1列为task,setTimeout2列为task。 【task队列:setTimeout1 setTimeout2;microtask队列:promise1】
  3. script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise1执行。

循环2:

【task队列:setTimeout1 setTimeout2;microtask队列:】

  1. 从task队列中取出setTimeout1,推入栈中执行,将promise2列为microtask。 【task队列:setTimeout2;microtask队列:promise2】
  2. 执行microtask checkpoint,取出microtask队列的promise2执行。

循环3:

【task队列:setTimeout2;microtask队列:】

  1. 从task队列中取出setTimeout2,推入栈中执行。
  2. setTimeout2任务执行完毕,执行microtask checkpoint。 【task队列:;microtask队列:】
process.nextTick(() => {
  console.log('nextTick')
})
Promise.resolve()
  .then(() => {
    console.log('then')
  })
setImmediate(() => {
  console.log('setImmediate')
})
console.log('end')

end
nextTick
then
setImmediate

CodingMeUp commented 4 years ago

Node与浏览器的 Event Loop 差异

浏览器环境 浏览器环境下的 异步任务 分为 宏任务(macroTask) 和 微任务(microTask):

当满足执行条件时,宏任务(macroTask) 和 微任务(microTask) 会各自被放入对应的队列:宏队列(Macrotask Queue) 和 微队列(Microtask Queue) 中等待执行。

Node 环境 在 Node 环境中 任务类型 相对就比浏览器环境下要复杂一些:

因此,也就产生了执行事件循环相应的任务队列 Timers Queue、I/O Queue、Check Queue 和 Close Queue。

循环之前

在进入第一次循环之前,会先进行如下操作:

开始循环

循环中进行的操作:

可以看出,nextTick 优先级比 Promise 等 microTask 高,setTimeout和setInterval优先级比setImmediate高

注意

在整个过程中,需要 注意 的是:

一句话总结其中:浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务

// 浏览器环境下:
while (true) {
    宏任务队列.shift();
    微任务队列全部任务();
}
// Node 环境下:
while (true) {
    loop.forEach((阶段) => {
        阶段全部任务();
        nextTick全部任务();
        microTask全部任务();
    });
    loop = loop.next;
}

function sleep(time) {
    let startTime = new Date();
    while (new Date() - startTime < time) {}
    console.log('<--Next Loop-->');
}

setTimeout(() => {
    console.log('timeout1');
    setTimeout(() => {
        console.log('timeout3');
        sleep(1000);
    });
    new Promise((resolve) => {
        console.log('timeout1_promise');
        resolve();
    }).then(() => {
        console.log('timeout1_then');
    });
    sleep(1000);
});

setTimeout(() => {
    console.log('timeout2');
    setTimeout(() => {
        console.log('timeout4');
        sleep(1000);
    });
    new Promise((resolve) => {
        console.log('timeout2_promise');
        resolve();
    }).then(() => {
        console.log('timeout2_then');
    });
    sleep(1000);
});

// 浏览器
timeout1
timeout1_promise
<--Next Loop-->
timeout1_then
timeout2
timeout2_promise
<--Next Loop-->
timeout2_then
timeout3
<--Next Loop-->
timeout4
<--Next Loop-->

// node
timeout1
timeout1_promise
<--Next Loop-->
timeout2
timeout2_promise
<--Next Loop-->
timeout1_then
timeout2_then
timeout3
<--Next Loop-->
timeout4
<--Next Loop-->

在 node 11 版本中,node 下 Event Loop 已经与浏览器趋于相同。

CodingMeUp commented 4 years ago

动态图

CodingMeUp commented 4 years ago