异步程序仍要回答两个问题:工作什么时候继续,结果是否还属于当前操作。Promise 表达未来的完成或失败,不会自动把计算搬到后台。async 函数在遇到 await 前仍同步执行;await 后的继续执行通过微任务安排。
先预测顺序
console.log("A");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
console.log("B");
// A, B, promise, microtask, timer
当前同步执行结束后,微任务检查点会继续处理队列中的微任务。零毫秒定时器表示满足调度条件后可运行,不保证零延迟。浏览器会在合适的机会更新渲染;不能把“每个任务结束必定绘制”当成规则。不断追加微任务也会饿死其他工作。
可运行顺序检查 使用 Node 的定时器和标准微任务 API 验证上面的有限场景。Node 还有自己的事件循环阶段与 nextTick,本例不证明两种宿主的全部顺序相同。
搜索框里的旧结果
用户输入 a 后又输入 ab,第二个请求可能先返回。仅按返回顺序 setState,会让旧结果覆盖新结果。可以给每次操作编号,并在提交结果前确认它仍是最新操作;AbortController 则帮助停止不再需要的工作。取消不是服务端事务回滚,也不能替代结果归属检查。
let latest = 0;
async function search(query) {
const version = ++latest;
const response = await fetch("/api/search?q=" + encodeURIComponent(query));
if (!response.ok) throw new Error("查询失败:" + response.status);
const result = await response.json();
if (version !== latest) return;
renderResults(result); // 这里由具体界面提供
}
上面是接入片段,renderResults 和接口需要应用提供。工程中还应把错误也关联到当前 version,避免旧失败覆盖新成功;页面卸载时取消任务或忽略结果。错误要在调用边界捕获,不能让按钮事件返回的 Promise 无人处理。
并行、顺序与失败
Promise.all 在一项拒绝后整体拒绝,但不会自动取消其他任务。allSettled 适合逐项展示结果。遍历时 for…of 配合 await 能按顺序执行;forEach 不会等待 async 回调。高并发下载需要限制同时工作的数量,否则会挤占连接、内存或服务端容量。
CPU 密集计算会阻塞运行它的线程。拆分任务能让出机会,Worker 可以使用独立执行环境;跨线程传输、复制和同步也有成本。
练习:模拟两个延迟不同的查询,让旧查询最后返回。先复现覆盖,再加编号保护,同时保证旧查询报错不改变当前错误提示。
面试自查:await 会阻塞整个线程吗?Promise.all 失败后其他请求是否停止?为什么防抖不能完全解决响应乱序?