浅谈不同环境下的JavaScript执行机制 + 示例详解

36次阅读

共计 5577 个字符,预计需要花费 14 分钟才能阅读完成。

概念
同步任务(Synchronous)
在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务(Asynchronous)
不进入主线程,而是进入“任务队列”的任务,只有主线执行栈清空,异步任务才进入主线执行栈执行
任务队列(Task Queue)
包含有异步任务的队列,包括“宏任务”与“微任务”
宏任务(Macrotasks / Task)

创建文档对象、解析 HTML、执行主线程代码(script)
执行各种事件:页面加载、输入、点击

setTimout,setInterval,setImmediate

I/O,Ajax,UI rendering

微任务(Microtasks / Jobs)

process.nextTick
Promise.then

Object.observe(已废弃)
MutationObserver

事件循环(Event Loop)
浏览器下的事件循环

事件循环是 js 实现异步的一种方法,也是 js 的执行机制
JavaScript 主线程会在执行栈清空后,读取任务队列,入栈第一个宏任务,主线程执行完该任务后又会先检查微任务队列并完成里面的所有微任务,包括新创建的微任务,完成一次事件循环。之后再次去读取任务队列,不断循环

注意:

每次循环只会入栈一个宏任务,所以多个宏任务需要多次事件循环才能执行完
每次循环会执行所有的微任务,所以每次循环结束后微任务队列被清空

Node.JS 下的事件循环

timers:
执行 setTimeout 和 setInterval 中到期的 callback

I/O callbacks:

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

setTimeout,setInterval,setImmediate 的 callback

用于执行 close 事件(关闭请求)的 callback,例如 socket.on(‘close’, callback)

idle, prepare:
libuv 内部调用

poll:

最为重要的阶段,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等
这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

check:
执行 setImmediate 的 callback

close callbacks:
执行 close 事件(关闭请求)的 callback,例如 socket.on(‘close’, callback)

事件循环的每一次循环都需要依次经过上述的阶段。每个阶段都有自己的 callback 队列,每当进入某个阶段,都会从所属的队列中取出 callback 来执行,当队列为空或者被执行 callback 的数量达到系统的最大数量时,进入下一阶段。这六个阶段都执行完毕称为一轮循环

注意:

不同于浏览器的是,在每个阶段完成后,microTask 队列就会被执行,而不是 MacroTask 任务完成后。
每个阶段完成后,微任务队列就会被执行。
如果在 timers 阶段执行时创建了 setImmediate 则会在此轮循环的 check 阶段执行,如果在 timers 阶段创建了 setTimeout,由于 timers 已取出完毕,则会进入下轮循环,check 阶段创建 timers 任务同理
递归的调用 process.nextTick 会导致 I/O starving,官方推荐使用 setImmediate

Node.JS 与浏览器下的差异

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

setTimeout(()=>{
console.log(‘timer1’)

Promise.resolve().then(function() {
console.log(‘promise1’)
})
}, 0)

setTimeout(()=>{
console.log(‘timer2’)

Promise.resolve().then(function() {
console.log(‘promise2’)
})
}, 0)

// 浏览器输出:
// time1
// promise1
// time2
// promise2

// Node 输出:
// time1
// time2
// promise1
// promise2
定时器
setTimeout(callback, time)

经过指定时间后,把要执行的任务 callback 加入到任务队列中
因为 JS 是单线程,任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间可能远远大于指定时间(time ms)

setTimeout(callback, 0)

指定某个任务 callback 在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行
0ms 实际上是不可能的,在浏览器中 setTimeout() / setInterval() 的每调用一次定时器的最小间隔 >=4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的 setInterval 的回调函数阻塞导致的
在 Node.JS 环境为 1ms,但也取决于系统当时的状况

setTimeout(function () {
console.log(“1”);
}, 0)

console.log(2)

// 输出 2 1
setInterval(callback, time)

每过指定时间(time ms),会有 callback 进入任务队列。
若 callback 执行时间超过了指定时间,那么就会导致 callback 连续执行,完全看不出来有时间间隔了

setImmediate(callback)
Node.JS 特有定时器,在事件循环的 check 阶段执行
process.nextTick(callback)

Node.JS 特有定时器,在事件循环各个阶段结束后执行
从技术上讲,它不是事件循环的一部分
同循环下 process.nextTick 会优于 Promise.then

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

// 输出 2 1
注意

连续的 setTimeout,setImmediate 在再 timer 阶段的执行顺序是不确定的,取决于系统当时的状况
但是把 setTimeout,setImmediate 放到一个 I/O 回调里面,就一定是 setImmediate 先执行,因为 poll 阶段后面就是 check 阶段

setImmediate(() => {
console.log(‘timer1’)

Promise.resolve().then(function () {
console.log(‘promise1’)
})
})

setTimeout(() => {
console.log(‘timer2’)

Promise.resolve().then(function () {
console.log(‘promise2’)
})
}, 0)

// Node 输出:
// timer1 timer2
// promise1 或者 promise2
// timer2 timer1
// promise2 promise1
fs.readFile(‘test.js’, () => {
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
})

// 输出 2 1

// 先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 下一次事件循环的 timers 阶段。因此,setImmediate 才会早于 setTimeout 执行。
示例
console.log(0)

new Promise(function(resolve) {
console.log(1);
resolve();
}).then(function() {
console.log(2)
})

setTimeout(function() {
console.log(3);
new Promise(function(resolve) {
console.log(4);
resolve();
}).then(function() {
console.log(5)
})
})

new Promise(function(resolve) {
console.log(6);
resolve();
}).then(function() {
console.log(7)
})

setTimeout(function() {
console.log(8);
new Promise(function(resolve) {
console.log(9);
resolve();
}).then(function() {
console.log(10)
})
})

console.log(11)
浏览器环境
第一轮事件循环
第一轮事件循环宏任务

开始执行代码,遇到 console.log 输出 0

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 1
遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数作为异步任务进入“宏任务队列”

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 6
遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数作为异步任务进入“宏任务队列”
最后遇到 console.log 输出 11
此时第一轮事件循环宏任务结束,依次输出 0 1 6 11

第一轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 2
遇到 then 执行其回调输出 7
此时第一轮事件循环微任务结束,依次输出 2 7

第一轮事件循环结束,此时“微任务队列”已被清空,“宏任务队列”里有两个 setTimeout 的回调函数,第一个 setTimeout 被送入主线程执行栈
第二轮事件循环
第二轮事件循环宏任务

开始执行第一个 setTimeout 回调函数

遇到 console.log 输出 3

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 4
遇到 then 回调函数作为异步任务进入“微任务队列”

此时第二轮事件循环宏任务结束,依次输出 3 4

第二轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 5
此时第二轮事件循环微任务结束,输出 5

第二轮事件循环结束,此时“微任务队列”已被清空,“宏任务队列”里剩下一个 setTimeout 的回调函数,其被送入主线程执行栈
第三轮事件循环
第三轮事件循环宏任务

开始执行第二个 setTimeout 回调函数

遇到 console.log 输出 8

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 9
遇到 then 回调函数作为异步任务进入“微任务队列”

此时第三轮事件循环宏任务结束,依次输出 8 9

第三轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 10
此时第三轮事件循环微任务结束,输出 10

第三轮事件循环结束,此时“微任务队列”已被清空,“宏任务队列”已被清空
至此整段代码执行完毕,完整输出结果为:0 1 6 11 2 7 3 4 5 8 9 10
Node.JS 环境

Node.JS 环境下任务队列有层级之分,按层级执行任务队列
第一轮事件循环
第一轮事件循环宏任务

开始执行代码,遇到 console.log 输出 0

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 1
遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数注册到 timer 阶段

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 6
遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数注册到 timer 阶段
最后遇到 console.log 输出 11
此时第一轮事件循环宏任务结束,依次输出 0 1 6 11

第一轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 2
遇到 then 执行其回调输出 7
此时第一轮事件循环微任务结束,依次输出 2 7

第一轮事件循环结束,此时“微任务队列”已被清空,timer 队列里有两个 setTimeout 的回调函数
第二轮事件循环
第二轮事件循环 timer 阶段

执行第一个 setTimeout 回调函数

遇到 console.log 输出 3

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 4
遇到 then 回调函数作为异步任务进入“微任务队列”

执行第二个 setTimeout 回调函数

遇到 console.log 输出 8

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 9
遇到 then 回调函数作为异步任务进入“微任务队列”

此时第二轮事件循 timer 阶段结束,依次输出 3 4 8 9

第二轮事件循环 timer 阶段微任务

执行注册在“微任务队列”里的微任务
遇到 then 执行其回调输出 5
遇到 then 执行其回调输出 10
此时第二轮事件循环 timer 阶段微任务结束,输出 5 10

第二轮事件循环结束,至此整段代码执行完毕,完整输出结果为:0 1 6 11 2 7 3 4 8 9 5 10
参考文章

阮一峰 – JavaScript 运行机制详解:再谈 Event Loop 阮一峰 – Node 定时器详解
Philip Roberts – Help,I’m stuck in an event loop
lynnelv – 深入理解 js 事件循环机制(Node.js 篇)
这一次,彻底弄懂 JavaScript 执行机制

正文完
 0