异步Bug成因解析:为何块级作用域const变量可修复该问题
异步Bug成因解析:为何块级作用域const变量可修复该问题
我来帮你拆解这个问题,其实核心是异步操作中的竞态条件加上JavaScript的变量执行顺序特性在搞鬼,咱们一步步把它说透。
先看原代码的问题出在哪
你写的countLetters函数里,result是外层的一个let变量,fruitList.map会生成多个并行执行的async回调,这些回调会同时对result做+=操作。问题就出在这个+=的执行顺序上:
当你直接写:
result += fruit + ": " + (await resolveName(fruit)).length + "\n";
这个表达式的执行分好几步:
- 先读取当前的
result值(初始是空字符串),把它暂存起来 - 然后计算后面的拼接字符串,但这里的
await resolveName(fruit)会让当前回调暂停,把控制权交还给事件循环 - 这时候其他map回调也在并行跑,它们也会读取
result的初始空值,然后同样因为await暂停 - 当所有
await都完成后,每个回调都会把自己计算的字符串,拼接到之前暂存的旧result值上,最后所有的赋值操作其实都是在空字符串的基础上做的——相当于每个回调都在执行result = "" + 自己的字符串,所以最后result只会保留最后一个完成的回调的结果。
为什么加const s就能修复?
当你改成先把结果存到块级作用域的const s里,再拼接到result:
const s = fruit + ": " + (await resolveName(fruit)).length + "\n"; result += s;
这个流程的逻辑变了:
- 先执行
await resolveName(fruit),拿到结果后计算出完整的字符串s(这一步是和当前fruit绑定的确定值) - 再执行
result += s——这一步是在await之后执行的,此时会读取result的最新值,然后把s拼接到后面 - 每个回调的
+=操作都是基于最新的result状态,不会再用之前暂存的旧值,自然就能按顺序把所有字符串都拼进去了。
更规范的写法:避免共享变量的竞态
其实官方给出的join()方案才是更稳妥的,从根源上避免共享变量的竞态问题:让每个map回调返回自己的字符串片段,用Promise.all收集所有片段后再拼接,代码更清晰也更可靠:
async function countLetters(fruitList) { const parts = await Promise.all( fruitList.map(async (fruit) => { return `${fruit}: ${(await resolveName(fruit)).length}\n`; }) ); return parts.join(''); }
最后再总结下
原代码的Bug本质是异步竞态条件:+=操作的「读取旧值」和「赋值新值」被await拆成了两步,导致多个并行回调基于同一个旧值修改共享变量;而用块级作用域的const s先缓存结果,能保证+=操作是基于最新的result状态执行,从而修复了竞态问题。当然,最推荐的还是用Promise.all收集结果再拼接的写法,从根源上规避这类共享变量的坑。
备注:内容来源于stack exchange,提问作者Anantara
相关产品推荐
相关产品推荐

