JavaScript 函数暂停与恢复:基于异常机制的异步实现原理

作者:半夏

时间:2026-10-23

浏览:608

本文深入解析 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程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章