浏览器和node的事件循环

Posted by CodingWithAlice on April 12, 2021

浏览器和node的事件循环

参考博客:浏览器和Node事件循环的区别

总结:

  • 浏览器和Node事件循环的区别

    image-20210820100642113

Event Loop即事件循环,是指浏览器 或 Node的一种 解决javaScript单线程运行时不会阻塞 的一种机制,也就是我们经常使用异步的原理。

一、浏览器事件循环

1、JS的主线程是【单线程】

但是 ajaxsetTimeout就会在浏览器中多开一条线程;

JS有一个主线程调用栈/执行栈 ,所有的任务都会被放到调用栈等待主线程执行;

JS调用栈 采用的是 后进先出 的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空;

任务队列采用的是先进先出的一种数据结构;

2、同步和异步

  • 同步和异步任务 分别进入不同的执行”场所” ,同步的进入主线程,异步的进入 Event Table 并注册函数
  • 同步任务会在调用栈中按照顺序等待主线程依次执行

  • 异步任务会在 异步任务有了结果后,Event Table会将回调函数移入 Event Queue

  • 主线程内的任务执行完毕(调用栈被清空),会去 Event Queue 读取对应的函数,进入主线程执行。

  • 上述过程会不断重复,也就是常说的 Event Loop(事件循环)。

两个定时函数

  • setTimeout

    是经过指定时间后,把要执行的任务加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于设置的时间长度。

  • setInterval

    会每隔 指定的时间将注册的函数置入Event Queue ,如果前面的任务耗时太久,那么同样需要等待。

    唯一需要注意的一点是,对于 setInterval(fn,ms) 来说,我们已经知道不是每过 ms 秒会执行一次fn,而是 每过ms秒,会有fn进入 Event Queue

    —> 一旦 setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了

3、宏任务和微任务

除了广义的同步任务和异步任务,我们对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码 scriptsetTimeoutsetIntervalsetImmediate(ie下生效) MessageChannel(消息通道),I/OUI Rendering
  • micro-task(微任务)Promise.thenasync/await),Process.nextTick(node的文法,比Promise.then执行的快),MutationObserver(监听dom节点更新完毕)
    • async/await :底层转换成了 Promisethen 回调函数 —> 每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中
    • Promise 的 thencatch 才是 microTask ,本身的内部代码不是

不同类型的任务会进入对应的Event Queue,比如setTimeoutsetInterval会进入相同的Event Queue。

进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

拼多多编程题:

image-20210810173806039

考点:

new Promise - 一注册就执行;

await 后面跟的函数是同步执行的,但是 async 中 await 之后的操作被放到了 then 方法中,即微任务中;

浏览器打印结果:

image-20210810174155786

编程题2:

image-20210830164543216

考点: Promise.resolve() 是一个同步方法,then 注册的方法才是微任务;

异步任务会先在 Event Table 中注册函数,但是只有结果返回后才会将函数推入事件队列时机】;

执行分析:

微任务队列内容: 1⃣️第一个微任务:

console.log(1);
Promise.resolve().then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(4);
})

2⃣️执行输出1 Promise.resolve()后,2被推入微任务执行队列 1输出后,5 被推入微任务执行队列

3⃣️执行输出2,3被推入微任务队列 执行输出5,6被推入微任务执行队列

4⃣️执行输出3,4被推入微任务执行队列 执行输出6

5⃣️执行输出4

浏览器打印结果:

image-20210830164635421

二、node事件循环

node 环境和 浏览器环境的区别
  • node - 事件循环实现依赖于 libuv 引擎
  • 浏览器 - V8引擎将 js 解析后调用对应的 node API -> 最终都是由 libuv 引擎驱动

循环之前:

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

  • 同步任务;
  • 发出异步请求;
  • 规划定时器生效的时间;
  • 执行process.nextTick()

开始循环:

循环中进行的操作:

  • 清空当前循环内的 Timers Queue,清空 NextTick Queue,清空 Microtask Queue
  • 清空当前循环内的 I/O Queue,清空 NextTick Queue,清空 Microtask Queue
  • 清空当前循环内的 Check Queue,清空 NextTick Queue,清空 Microtask Queue
  • 清空当前循环内的 Close Queue,清空 NextTick Queue,清空 Microtask Queue
  • 进入下轮循环。

可以看出,nextTick 优先级比 Promise 等 microTask 高,setTimeoutsetInterval优先级比setImmediate高。

node的事件环和我们浏览器的不太一样,它 给每一个任务都配了一个队列,如下图

image-20210705103116124

外部输入数据 —> 轮询阶段(poll) —> 检查阶段(check) —> 关闭事件回调阶段(close callback) —> 定时器检测阶段(timer) —> I/O事件回调阶段(I/O callbacks) —> 闲置阶段(idle, prepare) —> 轮询阶段…

  • timers: 执行定时器 setTimeoutseInterval 的回调

  • I/O callbacks: 这个阶段执行几乎所有的回调 - 是否有已完成的 I/O 操作的回调函数,来自上一轮的 poll 残留。但是 不包括 close事件,定时器和setImmediate()的回调

  • idle, prepare: 内部的一些事件。

  • poll: 轮循,i/o,回调,fs.readFile()。

    —> 先查看 poll queue 中是否有事件,有任务就按先进先出的顺序依次执行回调。

    —> 当 queue 为空时,

    ​ —> 会检查是否有 setImmediate()callback,如果有就进入 check 阶段执行这些 callback

    ​ 但同时也会检查是否有到期的 timer,如果有,就把这些到期的 timercallback 按照调用顺序放到 timer queue 中,之后循环会进入 timer 阶段执行 queue 中的 callback

    ​ 这 两者的顺序是不固定 的,收到代码运行的环境的影响。

    ​ —> 如果两者的queue都是空的,那么 loop 会在 poll 阶段 停留,直到有一个 i/o 事件返回,循环会进入 i/o callback 阶段并立即执行这个事件的 callback

  • check:执行 setImmediate 的回调。

  • close callbacks: 一些关闭的回调函数,例如 socket.on('close', ...)/socket.destory()

需要关心的就是 timers、poll、check 这三个阶段,执行顺序默认,会从上到下依次执行,

1、如果代码执行到 poll 后,发现check阶段没有,那就在poll在等待,等待 timer 时间到达后,再清空代码

2、队列(Timers QueueI/O QueueCheck QueueClose Queue)发生切换时,就会执行微任务

3、poll 的下一个阶段就是check,如果check队列中有东西的s,会先执行check

image-20210705200552595

三个常用于推迟任务执行的方法 - process.nextTick、setTimeout、setImmediate

这三者间存在着一些非常不同的区别

process.nextTick() setTimeout setImmediate
nextTick queue - 在每一个阶段执行完毕准备进入下一个阶段时优先执行(类似于微任务 -> 循环执行,直到清空) 定义一个回调,并且希望这个回调在我们所指定的时间间隔后 第一时间 去执行 -> 受到操作系统、当前执行任务的影响,并不会在预期的时间精准执行 立刻执行 - 在 poll 阶段之后才会执行回调

代码案例1:

// node 版本 12.13.1
setTimeout(()={
    console.log("time1");// --> 第3
    process.nextTick(()=>{
        console.log("nextTick2");// --> 第4【类似于微任务】
    });
});
console.log("start") // --> 第1
process.nextTick(()=>{
    console.log("nextTick1"); // --> 第2
    setTimeout(()={
        console.log("time2"); // --> 第5
    });
});

image-20210705161013892

代码案例2:在 node 中执行以下代码结果是什么 –> 答案是不一定【配合看案例3】

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

setImmediate(() => {
    console.log('immediate');
});

image-20210705164459438

代码案例3:【配合看案例2】

在一种情况下可以准确判断两个方法回调的执行顺序:在 同一个I/O事件的回调

因为在I/O事件的回调中,setImmediate 方法的回调永远在 timer 的回调前执行。

const fs = require('fs');
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});
// 答案永远是: immediate timeout