javascript是一门多线程的语言_彻底理解JavaScript中的Event Loop

一、前言

Javascript从诞生之日起,就一直是一门单线程的非阻塞的语言。即便后面出现了WebWorker,但是其本质上还是主线程的子线程,帮助主线程分担一部分计算任务,并非真正意义上的多线程。由于浏览器环境和Node环境在处理Event Loop上大致相同,但又有所区别,所以本文将区分开讲解

二、浏览器环境中的事件循环

1、事件循环

在浏览器中,事件循环过程可如下图所示:

在JavaScript脚本的执行过程中,遇到异步操作时:

主线程不会挂起等待异步操作执行完毕,而是将其挂起,然后继续执行之后的任务

当异步事件完成后,会将该事件加入到事件队列中

当主线程空闲时,会去查看事件队列,队列不为空时,主线程会逐个取出放入执行栈中执行

2、微任务(microtask)与宏任务(macro task)

异步任务之间并不是完全等同的,它们存在一个执行优先级。按照执行优先级,可区分为两类任务:微任务和宏任务,即:

微任务:promise、Object.observe、MutationObserver

宏任务:script、setTimeout、setInterval、I/O、UI rendering

在一次事件循环中,异步事件返回的结果会被放入到一个任务队列中,但是根据异步事件的类型,需要把事件放入到对应的微任务队列或宏任务队列中。

当主线程空闲时(执行栈为空),主线程会先查看微任务队列,执行清空后再查看宏任务队列,并执行清空,如此反复循环

总结而言,浏览器中事件循环就一句话:当前执行栈执行完成时,立即优先处理微任务,再去处理宏任务,同一次事件循环中,微任务先于宏任务执行,例子如下:

setTimeout(() => console.log('setTimeout'))

new Promise((resolve) => {

console.log('Promise')

resolve()

}).then(() => {

console.log('Then')

})

/*

输出:

Promise

Then

setTimeout

*/

例子2:

setTimeout(() => console.log(1), 0)

new Promise((resolve, reject) => {

console.log(2)

for (let i = 0; i < 1e4; i++) {

i === 9999 && resolve()

}

console.log(3)

}).then(() => {

console.log(4)

})

console.log(5)

/*

输出:2 3 5 4 1

*/

三、Node中的事件循环

相比浏览器中的事件循环,Node中的事件循环要复杂一些。在Node中,V8解析后的代码会通过Libuv执行,所以要理解Node中的事件循环,则就需要先理解Node的事件循环模型,如下:

不过,在外部输入数据到来时,是从poll阶段开始的,即:

外部数据 -> poll -> check -> close callbacks -> timer -> I/O callbacks

-> idle,prepare -> poll ...

而这些阶段的功能大致如下:

timer:执行定时器队列中的回调(如setTimeout、setInterval)

I/O callbacks:执行除了close事件、定时器、setImmediate之外的所有的回调

idel,prepare:仅内部使用,无需理会

poll:等待新的I/O事件,在一些特殊情况下,会阻塞在这里

check:专门执行setImmediate中的回调

close callbacks:执行close事件的回调,如socket.on('close', ...)

各阶段的详细介绍如下:

1、各个阶段详解

timer阶段

这个阶段会以先进先出的顺序执行所有到期后加入到timer queue里的回调(这些回调是通过setTimeout或者setInterval设置的)

I/O callback阶段

这个阶段主要执行大部分的I/O事件回调,包括为操作系统执行的一些回调(如TCP连接出错时,通过callback拿到错误信息)

poll阶段

V8引擎将JS代码解析后传入libuv,循环首先进入poll阶段,此后:

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

poll queue为空时,检查是否有setImmediate的callback,有则进入check阶段执行setImmediate的回调

检查是否有到期的timer,如果有,按timer的到期顺序放到timer queue中,此后进入到timer阶段时,执行timer queue中的回调

如果setImmediate和timer的队列都是空的,则事件循环停留在poll阶段,直到有I/O事件返回,事件循环才立即进入I/O callbacks阶段,并且立即执行回调

check阶段

本阶段专门用来执行setImmediate()的回调,当poll进入空闲状态,且setImmediate queue不为空时,事件循环会进入该阶段

close阶段

当一个socket连接或一个handle被突然关闭时(如调用了socket.destroy()),close事件会被发送到这个阶段执行回调,否则事件会用process.nextTick()方法发送出去

2、process.nextTick

虽然没有专门一个阶段来执行nextTick,但是存在一个nextTick queue,在事件循环 准备进入下一个阶段 前会先检查nextTick queue是否为空,不为空则需要等执行清空。

所以,如果错误地使用process.nextTick,会导致node进入死循环

3、例子讲解

例子1:

setTimeout(() => console.log(1), 0)

setImmediate(() => console.log(2))

// 输出:不确定,可能是1 2,也可能是2 1

主要看执行时环境:

定时器的时间取值范围为[1, (2^31)-1],所以setTimeout(fn, 0)等同于setTimeout(fn, 1)

如果timer阶段前的准备时间大于1ms,则setTimeout中的回调便能够进入timer queue,从而得以执行。之后,进入poll阶段,setImmediate的回调进入setImmediate queue,并在check阶段得以执行,故最终输出:1 2

如果timer阶段前的准备时间小于1ms,则初始timer阶段时timer queue为空。之后,进入poll阶段,setTimeout和setImmediate各自的回调进入各自的队列,之后check阶段执行setImmediate,timer阶段执行setTimeout,故输出:2 1

例子2:

const fs = require('fs');

fs.readFile(__filename, () => {

setTimeout(() => {

console.log('timeout');

}, 0);

setImmediate(() => {

console.log('immediate');

});

});

// 输出:immediate timeout

解析:

首先,fs.readFile里的回调属于I/O回调,在poll阶段时,由于timer queue和immediate queue都为空,所以在读取文件的操作完成后,进入到I/O callbacks阶段,执行I/O回调

I/O回调执行完成后,setImmediate和setTimeout先后加入到了timer queue和immediate queue里,之后先进入check阶段,后进入timer阶段,故输出:immediate timeout

例子3:

setInterval(() => {

console.log('setInterval')

}, 100)

process.nextTick(function tick () {

process.nextTick(tick)

})

// 死循环

解析:

process.nextTick会在每个阶段之后执行,并且队列不清空就不会进入下一个阶段

setInterval的执行时间为100ms,所以一开始的timer阶段中,timer queue为空,进入process.nextTick的清空过程

由于process.nextTick执行后不断地往nextTick queue加入回调,故永远无法清空,永远无法进入下一个阶段

例子4:

setInterval(() => {

console.log('setInterval')

}, 100)

setImmediate(function immediate () {

setImmediate(immediate)

})

// 每隔100ms输出一次setInterval

解析:在check阶段执行后,新设置的setImmediate回调会被放入到setImmediate queue中,而这个queue只能在下一次事件循环的check阶段进行清空,而在本次check阶段到下一次check阶段之间,timer阶段都会得以执行

例子5:

setImmediate((/* 回调1 */) => {

console.log('setImmediate1')

setImmediate(() => {

console.log('setImmediate2')

})

process.nextTick(() => {

console.log('nextTick')

})

})

setImmediate((/* 回调2 */) => {

console.log('setImmediate3')

})

/*

输出:

setImmediate1

setImmediate3

nextTick

setImmediate2

*/

解析:

第一次执行后,setImmediate queue中会依次设置两个回调:[回调1, 回调2]

check阶段执行两个回调,首先执行回调1:

首先,输出setImmediate1

其次,执行setImmediate(() => { console.log('setImmediate2') }),设置到下一次事件循环的setImmediate queue中

最后,将() => console.log('nextTick')设置到nextTick queue中

接着,执行回调2,输出:setImmediate3

check阶段结束,执行nextTick queue的清空,输出:nextTick,此后进入下一个事件循环

在新的事件循环的check阶段里,清空setImmediate queue,输出:setImmediate2

例子6:

const promise = Promise.resolve()

.then(() => {

return promise

})

promise.catch(console.error)

// 死循环

解析:在每个阶段的末尾,会清空microtasks queue

例子7:

const promise = Promise.resolve()

promise.then(() => {

console.log('promise')

})

process.nextTick(() => {

console.log('nextTick')

})

// 输出:nextTick promise

解析:promise.then 虽然和 process.nextTick 一样,都将回调函数注册到 microtask,但 process.nextTick 的 microtask queue 总是优先于 promise 的 microtask queue 执行

例子8:

setTimeout(() => {

console.log(1)

}, 0)

new Promise((resolve, reject) => {

console.log(2)

for (let i = 0; i < 10000; i++) {

i === 9999 && resolve()

}

console.log(3)

}).then(() => {

console.log(4)

})

console.log(5)

/*

输出:2 3 5 4 1

*/

例子9:

setImmediate((/* 回调1 */) => {

console.log(1)

setTimeout(() => {

console.log(2)

}, 100)

setImmediate(() => {

console.log(3)

})

process.nextTick(() => {

console.log(4)

})

})

process.nextTick((/* 回调2 */) => {

console.log(5)

setTimeout(() => {

console.log(6)

}, 100)

setImmediate(() => {

console.log(7)

})

process.nextTick(() => {

console.log(8)

})

})

console.log(9)

解析:

首先输出9,然后清空nextTick queue,此时的nextTick为:回调2

执行回调2:

输出:5

100ms后将() => console.log(6)加入timer queue

() => console.log(7)加入setImmediate queue

清空nextTick queue,输出:8

进入check阶段,此时setImmediate queue为:[回调1, () => console.log(7)]:

执行回调1:

输出:1

100ms后将() => console.log(2)加入timer queue

将() => console.log(3)加入setImmediate queue

执行() => console.log(7),输出:7

清空nextTick queue,输出:4

进入timer阶段,100ms未超时,进入下一阶段

进入check阶段,此时setImmediate queue为:[() => console.log(3)],输出:3

进入timer阶段,100ms到,此时timer queue为:[() => console.log(6), () => console.log(2)],依次清空,输出:6、2

所以最终输出:9 5 8 1 7 4 3 6 2

四、参考资料

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值