发布于 2026-01-06 0 阅读
0

承诺:五个提高工作效率的技巧 DEV 全球展示挑战赛,由 Mux 呈现:推介你的项目!

承诺:五个提高工作效率的技巧

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

在如今日益异步化的世界中,Promise 是强大的工具。它们帮助我们摆脱嵌套回调的繁琐,并简化程序流程。然而,关于 Promise 仍然存在许多误解,阻碍了开发者充分利用它们。

以下是一些可以提高项目代码质量和可读性的技巧。

1. 你不需要*那个匿名函数

Promise 的示例通常包括每个函数内部的匿名箭头函数.then(),但很多时候你并不需要它们。移除这些函数有助于展现 Promise 所提供的可读性和流程控制能力。

getZero()
  .then((number) => addOne(number))
  .then((number) => addTwo(number))
  .then((number) => addThree(number))
  .then((number) => console.log(number));

// Let's clean it up. Now we can read it like a sentence.
getZero()
  .then(addOne)
  .then(addTwo)
  .then(addThree)
  .then(console.log);
Enter fullscreen mode Exit fullscreen mode

以下是我参与的一个项目中的实际代码。这七行代码包含了程序的全部流程。无需阅读每个函数,我们就能非常清楚地了解程序的运行机制。(为了清晰起见,函数名已进行优化。)没有使用箭头函数或其他冗余代码来干扰程序行为的表达。

portUsed.check(port)
  .then(rejectIfUsed)
  .then(loadEnvironmentConfig)
  .then(startMonitor)
  .then(startServer)
  .then(logServerDetails)
  .catch(reportError);
Enter fullscreen mode Exit fullscreen mode

* 有时你需要匿名函数

当然,也有例外。当需要在 Promise 链之外访问值或需要保留方法上下文时,匿名函数可能非常有用甚至必不可少。虽然有一些方法可以解决这些问题,但它们并非总是更好或更简便。让我们来看看。

const state = {
  color: 'blue',
  setColor(newColor) {
    this.color = newColor;
  },
};

// We need some information not in the promise chain
getSystemData()
  .then((config) => showColorConfig(state.color, config));
Enter fullscreen mode Exit fullscreen mode

您可以使用.bind()部分应用和柯里化(这里没有介绍)等技术来避免使用匿名函数,但对于函数式编程经验较少的开发人员来说,这些技术可能比箭头函数更难理解。

// Bind example to access state outside the chain
getSystemData()
  .then(showColorConfig.bind(null, state.color));
Enter fullscreen mode Exit fullscreen mode

同样,使用匿名函数可以更方便地访问对象或类实例上的方法。这里也有其他方法。

// Regular method on an instance or object.
state.setColor('yellow');

// BAD, "state" context gets lost when setColor is called.
// "this" inside setColor is not what we expect.
getColor()
  .then(state.setColor);

// Good, preserve method context
getColor()
  .then((color) => state.setColor(color));

// Also OK, bind method context
getColor()
  .then(state.setColor.bind(state));
Enter fullscreen mode Exit fullscreen mode

这些选择往往取决于项目的惯例和开发人员的舒适度。

2. 承诺一切

Promise 的最大特性之一是其链式调用的延续性。任何非 Promise 函数的响应都会被封装在一个已解析的 Promise 中,因此 Promise 链会自动允许链中的任何函数是同步的还是异步的。

要获得这种好处,你只需从 Promise 开始即可。使用Promise.resolve()Promise 可以可靠地开启一个 Promise 链。之后,你无需知道某个操作是否是异步的;它就能正常工作。

Promise.resolve()
  .then(getFirstValue)
  .then(addOne)
  .then(addRandomValue)
  .then(saveValueToServer)
  .then(displayValue)
  .catch(displayError);
Enter fullscreen mode Exit fullscreen mode

乍一看,你无法分辨哪些函数是异步的,而且在这个层面上,这并不重要。你仍然可以理解代码的流程,它也能正常运行。随着项目的增长和变化,如果函数已经存在于 Promise 链中,那么将它们从同步改为异步就非常简单。

使用 `started` 语句启动 Promise 链Promise.resolve()意味着即使是 Promise 链中的第一个操作(几乎可以肯定是异步的)也可以替换为同步版本。这对于编写测试来说是一个巨大的优势。

我们可以用同步模拟方法替换任何异步方法,以检查程序的其余部分是否按预期运行。我们可以模拟已解决或已拒绝的 Promise,以适应不同的测试场景。而且,随着代码的演进,我们可以在不修改现有代码的情况下,轻松地在同步和异步之间切换每个函数。

3. 免费试用/捕捉

Promise 会将抛出的错误捕获为一个被拒绝的 Promise。这可以替代大多数 try/catch 逻辑。Promise 链中的每个函数都会被这种包装,因此无论哪个函数失败,我们都能得到错误处理。

使用 Async/await 要求我们在任何需要的地方添加错误处理,如果我们假设函数不会失败,仍然可能会出现未捕获的错误。如果项目后期更新引入了新的错误,这将带来更大的风险。

错误处理涉及很多细节和选项,让我们来详细了解一下。

基础知识

当代码非常简单时,我们可以直接用 try/catch 语句包裹所有代码。在这个层面上,try/catch 和 Promise 之间的区别微乎其微。

// Everything in try/catch for simplicity
try {
  const settings = await getUserSettings();
  const data = JSON.parse(settings);
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}

Promise.resolve()
  .then(getUserSettings)
  .then(JSON.parse)
  .then(displayUserSettings)
  .catch(displayErrorPage);
Enter fullscreen mode Exit fullscreen mode

补充说明:与state.setColor之前的示例不同,上面的示例JSON.parse不需要上下文。因此可以直接传递参数,而无需将其包装在匿名函数中。

可恢复错误

但如果我们有更多需求呢?如果程序可以从某些错误中恢复呢?或许我们希望在程序getUserSettings运行失败时显示默认用户设置。

使用 try/catch 代码块时,我们必须预先考虑到每一种可能出现的故障并进行处理。

let data;
try {
  const settings = await getUserSettings();
  // JSON.parse can throw an error, too.
  data = JSON.parse(settings);
} catch {
  data = useDefaultSettings();
}

try {
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}
Enter fullscreen mode Exit fullscreen mode

乘法误差

如果“可恢复”的 catch 代码块中的函数可能会出错,我们就需要嵌套 try/catch 代码块。为此,我们可以创建不同的函数来添加多层错误处理,而无需使用过多的缩进,以免代码难以理解。

// Separate logic to avoid nesting
const tryUserSettings = async () => {
  try {
    const settings = await getUserSettings();
    return JSON.parse(settings);
  } catch {
    return useDefaultSettings();
  }
};

try {
  // Handling more possibilities.
  const data = await tryUserSettings();
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}
Enter fullscreen mode Exit fullscreen mode

现在我们对所有环节都做了错误处理,但程序流程却变得难以理解。不过我们可以做得更好。因为 ` awaitasync/await` 适用于任何 Promise,我们可以将 Promise 链和 `async/await` 结合起来使用。

// Simple await.catch() pattern
const data = await mainAction().catch(backupAction);
Enter fullscreen mode Exit fullscreen mode

这种风格可以比使用多个 try/catch 块更整齐地将某些操作和错误处理程序分组在一起,同时保持所有逻辑的一致性。

// A bit of both - await a promise chain
// All of "get data" is now one block
//  and we catch errors from useDefaultSettings

try {
  const data = await getUserSettings()
    .then(JSON.parse)
    .catch(useDefaultSettings);
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}
Enter fullscreen mode Exit fullscreen mode

虽然花了一些时间,但我们已经处理了所有错误,甚至包括来自……的错误useDefaultSettings

承诺之路

让我们用 Promise 链来实现同样的逻辑。使用 Promise,我们可以在流程中.catch()随时添加回退机制或通用设置,以应对预期行为失败的情况。此外,我们还受益于技巧 2,即无需了解哪些函数是异步的。

Promise.resolve()
  .then(getUserSettings)
  .then(JSON.parse)
  .catch(useDefaultSettings)
  .then(displayUserSettings)
  .catch(displayErrorPage);
Enter fullscreen mode Exit fullscreen mode

Promise 可以用更少的代码处理错误,减少对函数的假设,并使程序流程更加清晰。

4. 避免意外序列化

async/await非常适合编写看似同步但实际上并非同步的代码。在某些情况下,这种看似同步的代码反而会导致延迟。使用纯 Promise 或 Promise 方法与 async/await 的组合可以使代码保持最佳性能。以下是一些问题的代码示例和一些替代方案。

// Accidental Serialization
const getProductDetails = async () => {
  // Await stops the code, so getProductList
  //  won't start until getUserSettings is done.
  const user = await getUserSettings();
  const products = await getProductList();

  // Did products need to wait for user?
  console.log(user, products);
};

// Prevent Serialization
const getProductDetails = async () => {
  // These variables are promises.
  // Both requests start immediately.
  const user = getUserSettings();
  const products = getProductList();

  // No waiting, but we must remember they are promises.
  console.log(await user, await products);
};

// Hybrid solution
const getProductDetails = async () => {
  // No serialization thanks to Promise.all.
  // await gets us out of the promise chain.
  const [user, products] = await Promise.all([
    getUserSettings(),
    getProductList(),
  ]);
  console.log(user, products);
};

// Only Promises - No async needed
const getProductDetails = () => {
  Promise.all([
    getUserSettings(),
    getProductList(),
  ]).then(([user, products]) => {
    console.log(user, products);
  });
};
Enter fullscreen mode Exit fullscreen mode

在项目中添加功能或增加复杂性时,经常会遇到这种情况。无论您是单独使用 Promise,还是将其与 async/await 结合使用,Promise 都是确保代码或用户不会等待过久的最佳选择之一。

5. 承诺并非万灵药

虽然 Promise 在封装复杂细节方面做得很好,使用户能够阅读程序的高级流程,但有些情况下,试图将所有内容都塞进 Promise 中反而会使代码变得混乱。像请求重试和轮询这类复杂或重复的行为,在 Promise 链之外可能更容易阅读和理解。

我发现,当你从项目的高层流程出发思考时,它们的效果最好。你可以使用简单的 Promise 链来勾勒出你希望代码实现的功能……

const eureka = () => Promise.resolve()
  .then(connectToHomeServer)
  .then(loadSmartDeviceList)
  .then(selectOnlyLights)
  .then(turnOnDevices);
Enter fullscreen mode Exit fullscreen mode

然后付诸行动。

文章来源:https://dev.to/oculus42/promises- Five-tips-to-work-smarter-mme