event-loop 事件轮询

javascript的事件轮询
JavaScript 是单线程的,但是如果完全由上至下的一行一行执行代码,假如一个代码块执行了很长的时间,后面必须要等待当前执行完毕,这样的效率是非常低的,所以有了异步的概念,确切的说,JavaScript 的主线程是单线程的,但是也有其他的线程去帮我们实现异步操作,比如定时器线程、事件线程、Ajax 线程。

最近看到这样一道有关事件循环的前端面试题:

async function async1() {
        console.log('async1 start')
        await async2()
        console.log('async1 end')
    }
    async function async2() {
        console.log('async2')
    }
    console.log('script start')
    setTimeout(function(){
        console.log('setTimeout')
    },0)
    async1()
    new Promise(function(resolve) {
        console.log('promise1')
        resolve()
    }).then(function(){
        console.log('promise2')
    })
    console.log('script end')

执行javascript的两个区域,
一个是同步代码执行,是在栈中执行,原则是先进后出
而且执行异步的时候分为两个队列 macro(task) 宏任务 和 micro(task)微任务 ,原则先进先出
宏任务
可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->…
(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
微任务
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

Promise和async中的立即执行
我们知道Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?

await做了什么
从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
回到本题
1、首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:
在这里插入图片描述
2、然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中:
在这里插入图片描述
3、script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。
在这里插入图片描述
4、script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。
在这里插入图片描述
5、script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。

根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。

因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 end和promise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
6、第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。

Node 中的事件轮询
在 Node 中的事件轮询机制与浏览器相似又不同,相似的是,同样先在栈中执行同步代码,同样是先进后出,不同的是 Node 有自己的多个处理不同问题的阶段和对应的队列,也有自己内部实现的微任务 process.nextTick,Node 的整个事件轮询机制是 Libuv 库实现的。

Node 中事件轮询的流程如下图:
在这里插入图片描述
从图中可以看出,在 Node 中有多个队列,分别执行不同的操作,而每次在队列切换的时候都去执行一次微任务队列,反复的轮询。

案例1

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

setImmediate(function() {
    console.log("setInmediate");
});

默认情况下 setTimeout 和 setImmediate 是不知道哪一个先执行的,顺序不固定,Node 执行的时候有准备的时间,setTimeout 延迟时间设置为 0 其实是大概 4ms,假设 Node 准备时间在 4ms 之内,开始执行轮询,定时器没到时间,所以轮询到下一队列,此时要等再次循环到 timer 队列后执行定时器,所以会先执行 check 队列的 setImmediate。
如果 Node 执行的准备时间大于了 4ms,因为执行同步代码后,定时器的回调已经被放入 timer 队列,所以会先执行 timer 队列。

案例 2

setTimeout(() => {
    console.log("setTimeout1");
    Promise.resolve().then(() => {
        console.log("Promise1");
    });
}, 0);

setTimeout(() => {
    console.log("setTimeout2");
}, 0);
console.log(1);
// 1
// setTimeout1
// setTimeout2
// Promise1

Node 事件轮询中,轮询到每一个队列时,都会将当前队列任务清空后,在切换下一队列之前清空一次微任务队列,这是与浏览器端不一样的。
浏览器端会在宏任务队列当中执行一个任务后插入执行微任务队列,清空微任务队列后,再回到宏任务队列执行下一个宏任务。
上面案例在 Node 事件轮询中,会将 timer 队列清空后,在轮询下一个队列之前执行微任务队列。

案例 3

setTimeout(() => {
    console.log("setTimeout1");
}, 0);

setTimeout(() => {
    console.log("setTimeout2");
}, 0);

Promise.resolve().then(() => {
    console.log("Promise1");
});
console.log(1);

// 1
// Promise1
// setTimeout1
// setTimeout2

上面代码的执行过程是,先执行栈,栈执行时打印 1,Promise.resolve() 产生微任务,栈执行完毕,从栈切换到 timer 队列之前,执行微任务队列,再去执行 timer 队列。

案例 4

setImmediate(() => {
    console.log("setImmediate1");
    setTimeout(() => {
        console.log("setTimeout1");
    }, 0);
});

setTimeout(() => {
    console.log("setTimeout2");
    setImmediate(() => {
        console.log("setImmediate2");
    });
}, 0);

//结果1
// setImmediate1
// setTimeout2
// setTimeout1
// setImmediate2

// 结果2
// setTimeout2
// setImmediate1
// setImmediate2
// setTimeout1

setImmediate 和 setTimeout 执行顺序不固定,假设 check 队列先执行,会执行 setImmediate 打印 setImmediate1,将遇到的定时器放入 timer 队列,轮询到 timer 队列,因为在栈中执行同步代码已经在 timer 队列放入了一个定时器,所以按先后顺序执行两个 setTimeout,执行第一个定时器打印 setTimeout2,将遇到的 setImmediate 放入 check 队列,执行第二个定时器打印 setTimeout1,再次轮询到 check 队列执行新加入的 setImmediate,打印 setImmediate2,产生结果 1。

假设 timer 队列先执行,会执行 setTimeout 打印 setTimeout2,将遇到的 setImmediate 放入 check 队列,轮询到 check 队列,因为在栈中执行同步代码已经在 check 队列放入了一个 setImmediate,所以按先后顺序执行两个 setImmediate,执行第一个 setImmediate 打印 setImmediate1,将遇到的 setTimeout 放入 timer 队列,执行第二个 setImmediate 打印 setImmediate2,再次轮询到 timer 队列执行新加入的 setTimeout,打印 setTimeout1,产生结果 2。

案例 5

setImmediate(() => {
    console.log("setImmediate1");
    setTimeout(() => {
        console.log("setTimeout1");
    }, 0);
});

setTimeout(() => {
    process.nextTick(() => console.log("nextTick"));
    console.log("setTimeout2");
    setImmediate(() => {
        console.log("setImmediate2");
    });
}, 0);

//结果1
// setImmediate1
// setTimeout2
// setTimeout1
// nextTick
// setImmediate2

// 结果2
// setTimeout2
// nextTick
// setImmediate1
// setImmediate2
// setTimeout1

这与上面一个案例类似,不同的是在 setTimeout 执行的时候产生了一个微任务 nextTick,我们只要知道,在 Node 事件轮询中,在切换队列时要先去执行微任务队列,无论是 check 队列先执行,还是 timer 队列先执行,都会很容易分析出上面的两个结果。

案例 6

const fs = require("fs");

fs.readFile("./.gitignore", "utf8", function() {
    setTimeout(() => {
        console.log("timeout");
    }, 0);
    setImmediate(function() {
        console.log("setImmediate");
    });
});

// setImmediate
// timeout

上面案例的 setTimeout 和 setImmediate 的执行顺序是固定的,前面都是不固定的,这是为什么?
因为前面的不固定是在栈中执行同步代码时就遇到了 setTimeout 和 setImmediate,因为无法判断 Node 的准备时间,不确定准备结束定时器是否到时并加入 timer 队列。
而上面代码明显可以看出 Node 准备结束后会直接执行 poll 队列进行文件的读取,在回调中将 setTimeout 和 setImmediate 分别加入 timer 队列和 check 队列,Node 队列的轮询是有顺序的,在 poll 队列后应该先切换到 check 队列,然后再重新轮询到 timer 队列,所以得到上面的结果。

案例 7

Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));

// nextTick
// Promise

在 Node 中有两个微任务,Promise 的 then 方法和 process.nextTick,从上面案例的结果我们可以看出,在微任务队列中 process.nextTick 是优先执行的。
上面内容就是浏览器与 Node 在事件轮询的规则,相信在读完以后应该已经彻底弄清了浏览器的事件轮询机制和 Node 的事件轮询机制,并深刻的体会到了他们之间的相同和不同。

详细的内容可以看这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值