本文最后更新于:2020年3月11日 上午
场景
SegmentFault
在使用 async-await
时,吾辈总是习惯把它们当作同步,终于,现在踩到坑里去了。
使用 setTimeout
和 setInterval
实现的基于 Promise
的 wait
函数,然而测试边界情况的时候却发现了一些问题!
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
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() } }) }
|
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| ;(async () => { let taskIsRun = false const add = async (_v, i) => { 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), ) console.log(Date.now() - start) })()
|
那么,先不要往下看,猜一下最后打印的大概会是多少呢?
实际执行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| 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
|
可以看到,很神奇的是 *判断后 => 执行前 => 判断后…=> 执行后…*,并不是预想中的 判断后 => 执行前 => 执行后… 的循环,所以,到底发生了什么呢?
思考
这个问题卡了吾辈两天之久,直到吾辈在 StackOverflow 提出的另一个相关的问题被外国友人回答了,瞬间吾辈就想起了 – async-await 本质上还是异步。
是的,为什么会出现 wait
一直在执行而后面的 taskIsRun = true
却并没有执行?因为 JavaScript 中的 async-await
虽然可以写出来很像同步代码的异步代码,但实际上还是异步的,原理还是基于 Promise
。
我们改造一下代码,将之使用原生 Promise
实现一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
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() } }) } ;(() => { let taskIsRun = false const add = (_v, i) => { return Promise.resolve() .then(() => { if (taskIsRun) { console.log(i + ' 判断前: ') return wait(() => !taskIsRun).then(() => { console.log(i + ' 判断后: ' + taskIsRun) }) } }) .then(() => { taskIsRun = true console.log(i + ' 执行前: ' + taskIsRun) return wait(100) }) .catch(() => {}) .then(() => { console.log(i + ' 执行后: ') taskIsRun = false }) }
const start = Date.now() Promise.all( Array(10) .fill(0) .map(add), ).then(() => console.log(Date.now() - start)) })()
|
这个时候就可以看出来了,判断逻辑是处在一个 then
后继里面的。那么,执行完 console.log(i + ' 判断后: ' + taskIsRun)
之后,就一定会继续执行下面的 then
函数么?并不,这时候 wait
函数内部实现中的 setInterval
还在运转,实际上 nodejs
并不会优先继续 then
这种 microtask
(微任务),而是会继续进行 setInterval
这种 macrotask
(宏任务)。这是 nodejs 与浏览器实现不一致的地方,吾辈将这些代码复制到浏览器上,确实可以正常执行并得到预期的结果。
微任务与宏任务参考
1 2 3 4 5 6
| if (taskIsRun) { console.log(i + ' 判断前: ') return wait(() => !taskIsRun).then(() => { console.log(i + ' 判断后: ' + taskIsRun) }) }
|
当然,nodejs 11 修复了这个问题,参考 https://github.com/nodejs/node/pull/22842。然而目前 NodeJS LTS 为 10,最新版本为 12,这个问题可能还要持续一段时间。
解决
那么,难道吾辈就必须等到 NodeJS LTS 最新版之后才能用 wait 么?或者说,吾辈就必须依赖于浏览器的 microtask/macrotask
么?并不,吾辈对之手动进行了处理即可!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
|
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 () => { let taskIsRun = false const add = async (_v, i) => { if (taskIsRun) { console.log(i + ' 判断前: ') await wait(() => { const result = !taskIsRun if (result) { taskIsRun = true } return result }) 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), ) console.log(Date.now() - start) })()
|
吾辈在 wait
函数中,即 setInterval
循环调用的函数中对 taskIsRun
进行了修改,而不是在 wait
后面,即 then
之后的 microtask
中进行修改,结果便一切如同吾辈所期待的一样了!