1

使用 setInterval 实现的 wait 执行顺序存在问题?

场景

使用 setTimeoutsetInterval 实现的基于 Promisewait 函数,然而测试边界情况的时候却发现了一些问题!

实现代码

/**
 * 等待指定的时间/等待指定表达式成立
 * 如果未指定等待条件则立刻执行
 * @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​
rxliuli 420
2019-05-14 提问
1 个回答
2

已采纳

这是因为在 Node(^10)1 和 Chrome 中的事件循环机制不一样导致的.你把代码放到 Chrome 中执行,就可以得到想要的大于 1000ms 的结果了.

详细一点点的解释

在事件循环中有Microtasks队列和Macrotasks队列

setTimeout任务和setInterval任务会被放入Macrotasks
Promise任务会被放入Microtasks

在 Chrome 中,会在每个Macrotasks执行完之后去执行Microtasks,也就是每个setTimeoutsetInterval执行完之后都去执行Promise.
而在 Node 中,则会在各个阶段之间去执行Microtasks,这就导致当执行完一个setTimeout之后,不一定接着就执行Promise.

你的代码按照你想要的结果是,在执行完setTimeoutsetInterval中的resolve()之后,会回到add中,将taskIsRun设置为true.
而 Node 中由于机制不同,当resolve()执行完之后,会接着执行下一个setInterval,导致这个时候taskIsRun还是false,所以setInterval中的判断条件!taskIsRun成立,就执行resolve().

if (taskIsRun) {
  await wait(() => {
    console.log(i, taskIsRun) // 添加这一行
    return !taskIsRun
  })
}

你可以taskIsRuntrue的执行语句改成上面这样,然后分别在 Node 和 Chrome 输出看看,在 Chrome 中能看到taskIsRun会一个个变为false,而在 Node 中,则会一下子好几个变为false.
如果在 Node 中执行多次的话,还能发现,每次变化的数量还不一样,有的时候全部变为false,有的时候是四五个.这是因为执行的速度不确定,导致有可能所有setInterval都被分配到一次循环中,也有可能被分到不同的循环中

如果要在 Node 中也达到想要的效果的话,可以在每次resolve()之前将taskIsRun设置为true,这样不管是在 Node 还是 Chrome 里执行,下一个setInterval里执行param()taskIsRun都一定为true,直到当前这个Promise执行到taskIsRun = false

1: 在 Node v11.0 之后,Node 会在执行每个单独的TimerImmediate之后去执行Microtasks,表现上会跟在 Chrome 中一致,详见timers: run nextTicks after each immediate and timer

扩展阅读

撰写答案

推广链接