说到同步执行异步任务,最先想到的就是 Promise
和 async/await
等常见方式 —— 它们通过语法层面的封装,使得异步代码看起来像是同步执行,极大地提升了代码的可读性和可维护性。但除了这些还有其它方式吗?当然有!
本文将介绍一种特殊的做法,借助异常机制将异步操作“同步化”,适用于一些控制流调度、SSR 框架内部等高级用法。
目标场景
首先定义一个异步请求函数
const globalData = {
request: function () {
return new Promise(resolve => {
setTimeout(() => {
resolve("success");
}, 1000);
});
},
};
以同步的方式调用它,拿到的是一个 Promise
,并不能直接拿到结果。
function main() {
console.log("main start");
const result = globalData.request();
console.log("result", result); // Promise {...}
}
但是最终的效果是期望直接拿到结果而不是 Promise
,该如何实现呢?
核心实现
实现思路:用异常“中断”,用缓存“同步”
- 劫持异步函数,首次调用时直接
throw Promise
暂停当前函数的执行; - 捕获该 Promise,等它完成后缓存结果,再重新执行函数。
React 的服务器端渲染(React Suspense + RSC)使用的就是这个机制。
第一步:劫持异步函数
改写 globalData.request
函数,使其在首次调用时抛出一个 Promise。等到 Promise 完成后,结果会被写入缓存。
function run(fun) {
const oldRequest = globalData.request;
const cache = {
status: 'pending', // 'pending', 'fulfilled'
value: null,
};
function newRequest(...orgs) {
if (cache.status === 'fulfilled') {
return cache.value;
}
const p = oldRequest(...orgs)
.then((res) => {
cache.status = 'fulfilled';
cache.value = res;
});
throw p;
}
globalData.request = newRequest;
...
}
第二步:捕获 Promise,重试执行函数
拿到捕获的 Promise
,当 Promise
完成时再次调用函数获取缓存中的结果。
function run(fun) {
...
try {
fun(); // 运行 main()
} catch (err) {
if (err instanceof Promise) {
err.then(() => {
globalData.request = newRequest; // 再次劫持
fun(); // 重试执行
}).catch((e) => {
console.error("Error occurred in the promise:", e);
}).finally(() => {
globalData.request = oldRequest; // 恢复原始函数
});
}
}
globalData.request = oldRequest;
}
运行结果
run(main);
# 第一次执行输出
main start
# 第二次执行输出
main start
result success
总结
通过“抛出异常 + 缓存结果 + 重试”的方式,实现了异步任务的“同步化”,这背后的思路其实并不复杂:用异常中断当前流程,用缓存提供未来值。。
虽然这种做法非常巧妙,但是并不适合日常业务代码,更适合在框架设计、底层架构中使用。不过可以当作一种思路储备,尤其是在面试中被问到时,能答出来绝对是一大亮点!