承诺:五个提高工作效率的技巧
由 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);
以下是我参与的一个项目中的实际代码。这七行代码包含了程序的全部流程。无需阅读每个函数,我们就能非常清楚地了解程序的运行机制。(为了清晰起见,函数名已进行优化。)没有使用箭头函数或其他冗余代码来干扰程序行为的表达。
portUsed.check(port)
.then(rejectIfUsed)
.then(loadEnvironmentConfig)
.then(startMonitor)
.then(startServer)
.then(logServerDetails)
.catch(reportError);
* 有时你需要匿名函数
当然,也有例外。当需要在 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));
您可以使用.bind()部分应用和柯里化(这里没有介绍)等技术来避免使用匿名函数,但对于函数式编程经验较少的开发人员来说,这些技术可能比箭头函数更难理解。
// Bind example to access state outside the chain
getSystemData()
.then(showColorConfig.bind(null, state.color));
同样,使用匿名函数可以更方便地访问对象或类实例上的方法。这里也有其他方法。
// 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));
这些选择往往取决于项目的惯例和开发人员的舒适度。
2. 承诺一切
Promise 的最大特性之一是其链式调用的延续性。任何非 Promise 函数的响应都会被封装在一个已解析的 Promise 中,因此 Promise 链会自动允许链中的任何函数是同步的还是异步的。
要获得这种好处,你只需从 Promise 开始即可。使用Promise.resolve()Promise 可以可靠地开启一个 Promise 链。之后,你无需知道某个操作是否是异步的;它就能正常工作。
Promise.resolve()
.then(getFirstValue)
.then(addOne)
.then(addRandomValue)
.then(saveValueToServer)
.then(displayValue)
.catch(displayError);
乍一看,你无法分辨哪些函数是异步的,而且在这个层面上,这并不重要。你仍然可以理解代码的流程,它也能正常运行。随着项目的增长和变化,如果函数已经存在于 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);
补充说明:与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);
}
乘法误差
如果“可恢复”的 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);
}
现在我们对所有环节都做了错误处理,但程序流程却变得难以理解。不过我们可以做得更好。因为 ` awaitasync/await` 适用于任何 Promise,我们可以将 Promise 链和 `async/await` 结合起来使用。
// Simple await.catch() pattern
const data = await mainAction().catch(backupAction);
这种风格可以比使用多个 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);
}
虽然花了一些时间,但我们已经处理了所有错误,甚至包括来自……的错误useDefaultSettings。
承诺之路
让我们用 Promise 链来实现同样的逻辑。使用 Promise,我们可以在流程中.catch()随时添加回退机制或通用设置,以应对预期行为失败的情况。此外,我们还受益于技巧 2,即无需了解哪些函数是异步的。
Promise.resolve()
.then(getUserSettings)
.then(JSON.parse)
.catch(useDefaultSettings)
.then(displayUserSettings)
.catch(displayErrorPage);
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);
});
};
在项目中添加功能或增加复杂性时,经常会遇到这种情况。无论您是单独使用 Promise,还是将其与 async/await 结合使用,Promise 都是确保代码或用户不会等待过久的最佳选择之一。
5. 承诺并非万灵药
虽然 Promise 在封装复杂细节方面做得很好,使用户能够阅读程序的高级流程,但有些情况下,试图将所有内容都塞进 Promise 中反而会使代码变得混乱。像请求重试和轮询这类复杂或重复的行为,在 Promise 链之外可能更容易阅读和理解。
我发现,当你从项目的高层流程出发思考时,它们的效果最好。你可以使用简单的 Promise 链来勾勒出你希望代码实现的功能……
const eureka = () => Promise.resolve()
.then(connectToHomeServer)
.then(loadSmartDeviceList)
.then(selectOnlyLights)
.then(turnOnDevices);
然后付诸行动。
文章来源:https://dev.to/oculus42/promises- Five-tips-to-work-smarter-mme