JavaScript 函数暂停与恢复:基于异常机制的异步实现原理
本文深入解析 JavaScript 中函数的暂停与恢复机制,探讨如何利用 throw 和 catch 模拟 async/await 的执行流。通过构建中间层 runtime,拦截 fetch 调用并抛出异常以暂停函数,利用缓存机制在异步结果返回后恢复执行,从而在同步代码风格下实现异步逻辑。文章还涉及纯函数处理与副作用隔离,并关联 React Suspense 的设计灵感。
JavaScript 异步编程的挑战与 async/await 的局限
本文通过具体实例详解 JavaScript 中函数的暂停与恢复技术,旨在为读者提供相关参考与实现思路。
JavaScript 的异步编程长期被视为难点。早期开发者依赖回调函数(callback),但随之产生了“回调地狱”问题。随后 Promise 被引入以解决深层嵌套回调的困境。然而,过度使用 Promise(如链式 then)导致代码可读性下降。此时,async-await 语法应运而生,它允许以同步代码的编写方式处理异步逻辑,极大提升了代码清晰度,甚至被部分开发者视为 JavaScript 异步编程的终极解决方案。
注:文中代码仅用于演示原理,不具备生产环境可用性。
function getProfile(id) {
return window.fetch(`https://api.com/wedding/profile/${weddingId}`
}
async function getWeddingDetail(weddingId) {
try {
// 暂停执行
const wedding = await window.fetch(`https://api.com/wedding/${weddingId}`);
// 当结果返回恢复执行,接着继续暂停
const groom = await getProfile(wedding.groomId);
// ... 恢复执行 -> 暂停 ...
const bride = await getProfile(wedding.brideId);
// ... 恢复执行
return { wedding, bride, groom };
} catch (error) {
handleError(error);
}
}
实际上,计算机科学领域并不存在所谓的“银弹”。
尽管 async-await 备受推崇,但它并非完美无缺。例如,开发者容易遗忘添加 await 关键字。
假设存在一个名为 getProfile 的工具函数,若不了解其内部实现细节,开发者可能会误将其视为同步函数,即使它实际上执行的是异步操作。
这仅是一个小问题。更令人困扰的是,一旦在某个函数中使用了 async 关键字,调用该函数的外层函数也必须声明为 async。如果还有更外层的函数需要调用这个外层函数,这种“async 传染性”将导致代码结构变得极其复杂。
是否存在一种既能保持同步代码风格,又能处理异步逻辑的两全其美的方法?
// getWeddingDetail 根本不用关心内部的函数是异步or同步
function getWeddingDetail(weddingId) {
const wedding = window.fetch(`https://api.com/wedding/${weddingId}`);
const groom = getProfile(wedding.groomId);
const bride = getProfile(wedding.brideId);
return { wedding, bride, groom };
}
引入一个中间层或许能解决这一矛盾。
异步编程的核心在于函数的暂停与恢复执行。决定函数何时暂停或恢复是 JavaScript 运行时的职责,难道我们需要深入引擎底层实现吗?
不需要。我不具备 C++ 知识,也不了解 JavaScript 引擎的具体实现细节。
但是,我可以编写一个中间层(函数 runtime),尝试实现上述需求,尽管这会带来一些限制。
基于异常机制的函数暂停与恢复实现
一、入口函数设计
假设我们需要运行如下函数:
function main() {
const id = 123;
console.log('Getting wedding:', id);
const { wedding, bride, groom } = getWeddingDetail(id);
console.log('Wedding detail:', wedding);
}
我们期望能够以如下方式调用该函数:
function runtime(mainFn) {
mainFn();
}
// start runtime
runtime(main);
基本框架已搭建完毕,接下来该如何优化?
首先,需要明确在不使用 await 的前提下,如何中断函数的运行。
其次,需要在合适的时机恢复函数的执行。
JavaScript 中有两种中断函数运行的方式:return 和 throw。我选择使用 throw,因为它更能体现因遭遇异常而导致的中断状态。据此,我们对 runtime 进行改造。
function runtime(mainFn) {
const _originalFetch = window.fetch;
window.fetch = (url, options) => {
// "暂停"
throw new Error();
};
// 运行入口函数
runMain();
function runMain() {
try {
mainFn();
} catch (error) {
// 函数 "暂停"
// 恢复并重新执行 mainFn
runMain();
}
}
}
暂且忽略这段代码存在的问题,重点关注函数“中断”与“恢复”这两个关键点,显然初步目的已达到。接下来对其进行优化。
首要改进 runMain 函数,使其仅在 window.fetch 成功返回结果后再次执行:
function runtime(mainFn) {
const _originalFetch = window.fetch
window.fetch = (url, options) => {
_originalFetch(url, options).then(res => {
// 返回结果后恢复执行
runMain()
})
throw new Error()
}
runMain();
function runMain() {
try {
mainFn();
} catch (error) {
// ignore
}
}
}
由于 window.fetch 每次调用都抛出异常,这导致 mainFn 陷入无限循环执行。
为解决此问题,需要引入缓存机制。确保仅在第一次 fetch 时抛出异常,而在后续请求中直接返回缓存的响应。
function runtime(mainFn) {
const _originalFetch = window.fetch
windo.fetch = (url, options) => {
if (cache.has([url, options])) return cache.get([url, options])
_originalFetch(url, options).then(res => {
cache.set([url, options], res)
runMain()
})
throw new Error()
}
runMain();
function runMain() {
try {
mainFn();
} catch (error) {
// ignore
}
}
}
实现成功!
运行程序并检查 console 输出,由于函数被重复执行了多次,'Getting wedding:', 123 也被显示了多次,这是 console.log 产生的副作用所致。
纯函数处理与副作用隔离
二、纯函数处理
runtime 仅允许运行纯函数。如果代码中包含副作用,则必须添加限制条件:使用 runSideEffects() 进行包装。
function main() {
const id = 123;
runSideEffects(() => console.log('Getting wedding:', id));
const { wedding, bride, groom } = getWeddingDetail(id);
runSideEffects(() => console.log('Wedding detail:', wedding));
}
sideEffects 的实现方式如下:
function runtime(mainFn) {
// 参考上面的代码
// 提供 `runSideEffects`
const sideEffects = [];
window.runSideEffects = fn => {
sideEffects.push(fn);
};
runMain();
function runMain() {
try {
mainFn();
sideEffects.forEach(fn => fn());
} catch (error) {
// 清除副作用
sideEffects.splice(0, sideEffects.length);
}
}
}
再次运行程序,'Getting wedding:', 123 现在仅显示一次。
具体实现了什么功能?
为了模拟函数的暂停与恢复,我们通过 throw 一个错误来“暂停”函数,并通过重新运行来“恢复”函数。
为了实现从暂停处“恢复”,需要将抛出的错误替换为函数的返回值,我们通过缓存机制达成了这一目的。
最后,为了确保函数能够安全地重复执行,需要将其转化为一个纯函数。如果有副作用,则将它们收集起来,在函数运行成功后再统一执行副作用。
实际应用场景与 React Suspense 关联
阐述这么多原理,有什么实际应用场景?
本文的灵感来源于 React Suspense。有了 Suspense,数据获取可以像下面这样编写:
function Component() {
const data = getDataFromNetwork();
return <div />;
}
getDataFromNetwork 将发起异步请求,因此它是一个异步函数,但 React 使其看起来像是同步操作。这非常有趣。
原文阅读:pause-and-resume-a-javascript-function
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容可查看本站专题:《JavaScript常用函数技巧汇总》、《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
