使用 setInterval 实现的 wait 执行顺序存在问题?
场景
使用 setTimeout
和 setInterval
实现的基于 Promise
的 wait
函数,然而测试边界情况的时候却发现了一些问题!
实现代码
/**
* 等待指定的时间/等待指定表达式成立
* 如果未指定等待条件则立刻执行
* @param {Number|Function} [param] 等待时间/等待条件
* @returns {Promise} Promise 对象
*/
export const wait = param => {
return new Promise(resolve => {
if (typeof param === 'number') {
setTimeout(resolve, param)
} else if (typeof param === 'function') {
const timer = setInterval(() => {
if (param()) {
clearInterval(timer)
resolve()
}
}, 100)
} else {
resolve()
}
})
}
测试代码
;(async () => {
// 标识当前是否有异步函数 add 在运行了
let taskIsRun = false
const add = async (_v, i) => {
// 如果已经有运行的 add 函数,则等待
if (taskIsRun) {
console.log(i + ' 判断前: ')
await wait(() => {
return !taskIsRun
})
console.log(i + ' 判断后: ' + taskIsRun)
}
try {
taskIsRun = true
console.log(i + ' 执行前: ' + taskIsRun)
await wait(100)
} finally {
console.log(i + ' 执行后: ')
taskIsRun = false
}
}
const start = Date.now()
await Promise.all(
Array(10)
.fill(0)
.map(add)
)
// 执行 10 次的话应该超过 1000ms 才是,结果才 400+ms?
console.log(Date.now() - start)
})()
执行结果
0 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
1 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
2 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
3 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
4 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
5 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
6 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
7 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
8 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
9 判断前: at i + ' 判断前: ' src/module/function/wait.js:29:6
0 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
1 判断后: false at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
1 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
// 这儿的 1 执行前,结果 2 就已经判断通过并准备执行了???发生了什么?
2 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
2 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
3 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
3 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
4 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
4 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
5 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
5 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
6 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
6 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
7 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
7 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
8 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
8 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
9 判断后: true at i + ' 判断后: ' + taskIsRun src/module/function/wait.js:33:6
9 执行前: true at i + ' 执行前: ' + taskIsRun src/module/function/wait.js:37:6
1 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
2 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
3 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
4 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
5 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
6 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
7 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
8 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
9 执行后: at i + ' 执行后: ' src/module/function/wait.js:40:6
307 at Date.now() - start src/module/function/wait.js:52:2
这是因为在 Node(^10)1 和 Chrome 中的事件循环机制不一样导致的.你把代码放到 Chrome 中执行,就可以得到想要的大于 1000ms 的结果了.
详细一点点的解释
在事件循环中有
Microtasks
队列和Macrotasks
队列setTimeout
任务和setInterval
任务会被放入Macrotasks
中Promise
任务会被放入Microtasks
中在 Chrome 中,会在每个
Macrotasks
执行完之后去执行Microtasks
,也就是每个setTimeout
或setInterval
执行完之后都去执行Promise
.而在 Node 中,则会在各个阶段之间去执行
Microtasks
,这就导致当执行完一个setTimeout
之后,不一定接着就执行Promise
.你的代码按照你想要的结果是,在执行完
setTimeout
或setInterval
中的resolve()
之后,会回到add
中,将taskIsRun
设置为true
.而 Node 中由于机制不同,当
resolve()
执行完之后,会接着执行下一个setInterval
,导致这个时候taskIsRun
还是false
,所以setInterval
中的判断条件!taskIsRun
成立,就执行resolve()
.你可以
taskIsRun
为true
的执行语句改成上面这样,然后分别在 Node 和 Chrome 输出看看,在 Chrome 中能看到taskIsRun
会一个个变为false
,而在 Node 中,则会一下子好几个变为false
.如果在 Node 中执行多次的话,还能发现,每次变化的数量还不一样,有的时候全部变为
false
,有的时候是四五个.这是因为执行的速度不确定,导致有可能所有setInterval
都被分配到一次循环中,也有可能被分到不同的循环中如果要在 Node 中也达到想要的效果的话,可以在每次
resolve()
之前将taskIsRun
设置为true
,这样不管是在 Node 还是 Chrome 里执行,下一个setInterval
里执行param()
时taskIsRun
都一定为true
,直到当前这个Promise
执行到taskIsRun = false
扩展阅读
Node 和 Chrome 事件循环差异
js 事件循环