摘要:尽管可以让代码更加简洁易读,但对于只熟悉回调函数的人来说,可能对此还是会有所怀疑。始终避免在或使用回调函数,否则会吞噬任何后续的错误,将其作为链的一部分。然而,使用回调函数,使用所谓的,即第一个参数是一个错误回调变得很常见。
原文:ES6 Promises: Patterns and Anti-Patterns
作者:Bobby Brennan
当几年前,第一次使用 NodeJS 的时候,对现在被称为“ 回调地狱 ”的写法感到很困扰。幸运的是,现在是 2017 年了,NodeJS 已经采用大量 JavaScript 的最新特性,从 v4 开始已经支持 Promise。
尽管 Promise 可以让代码更加简洁易读,但对于只熟悉回调函数的人来说,可能对此还是会有所怀疑。在这里,将列出我在使用Promise 时学到的一些基本模式,以及踩的一些坑。
注意:在本文中将使用箭头函数 ,如果你还不是很熟悉,其实很简单,建议先读一下使用它们的好处
模式与最佳实践 使用 Promise如果使用的是已经支持 Promise 的第三方库,那么使用起来非常简单。只需关心两个函数:then() 和 catch()。例如,有一个客户端 API 包含三个方法,getItem(),updateItem(),和deleteItem(),每一个方法都返回一个 Promise:
Promise.resolve() .then(_ => { return api.getItem(1) }) .then(item => { item.amount++ return api.updateItem(1, item); }) .then(update => { return api.deleteItem(1); }) .catch(e => { console.log("error while working on item 1"); })
每次调用 then() 会在 Promise 链中创建一个新的步骤,如果链中的任何一个地方出现错误,就会触发接下来的 catch() 。then() 和 catch() 都可以返回一个值或者一个新的 Promise,结果将被传递到 Promise 链的下一个then()。
为了比较,这里使用回调函数来实现相同逻辑:
api.getItem(1, (err, data) => { if (err) throw err; item.amount++; api.updateItem(1, item, (err, update) => { if (err) throw err; api.deleteItem(1, (err) => { if (err) throw err; }) }) })
要注意的第一个区别是,使用回调函数,我们必须在过程的每个步骤中进行错误处理,而不是用单个的 catch-all 来处理。回调函数的第二个问题更直观,每个步骤都要水平缩进,而使用 Promise 的代码则有显而易见的顺序关系。
回调函数 Promise 化需要学习的第一个技巧是如何将回调函数转换为 Promise。你可能正在使用仍然基于回调的库,或是自己的旧代码,不过不用担心,因为只需要几行代码就可以将其包装成一个 Promise。这是将 Node 中的一个回调方法 fs.readFile 转换为 Promise的示例:
function readFilePromise(filename) { return new Promise((resolve, reject) => { fs.readFile(filename, "utf8", (err, data) => { if (err) reject(err); else resolve(data); }) }) } readFilePromise("index.html") .then(data => console.log(data)) .catch(e => console.log(e))
关键部分是 Promise 构造函数,它接收一个函数作为参数,这个函数有两个函数参数:resolve 和 reject。在这个函数里完成所有工作,完成之后,在成功时调用 resolve,如果有错误则调用 reject。
需要注意的是只有一个resolve 或者 reject 被调用,即应该只被调用一次。在我们的示例中,如果 fs.readFile 返回错误,我们将错误传递给 reject,否则将文件数据传递给resolve。
Promise 的值ES6 有两个很方便的辅助函数,用于通过普通值创建 Promise:Promise.resolve() 和 Promise.reject()。例如,可能需要在同步处理某些情况时一个返回 Promise 的函数:
function readFilePromise(filename) { if (!filename) { return Promise.reject(new Error("Filename not specified")); } if (filename === "index.html") { return Promise.resolve("Hello!
"); } return new Promise((resolve, reject) => {/*...*/}) }
注意,虽然可以传递任何东西(或者不传递任何值)给 Promise.reject(),但是好的做法是传递一个Error。
并行运行Promise.all是一个并行运行 Promise 数组的方法,也就是说是同时运行。例如,我们有一个要从磁盘读取文件的列表。使用上面创建的 readFilePromise 函数,将如下所示:
let filenames = ["index.html", "blog.html", "terms.html"]; Promise.all(filenames.map(readFilePromise)) .then(files => { console.log("index:", files[0]); console.log("blog:", files[1]); console.log("terms:", files[2]); })
我甚至不会使用传统的回调函数来尝试编写与之等效的代码,那样会很凌乱,而且也容易出错。
串行运行有时同时运行一堆 Promise 可能会出现问题。比如,如果尝试使用 Promise.all 的 API 去检索一堆资源,则可能会在达到速率限制时开始响应429错误。
一种解决方案是串行运行 Promise,或一个接一个地运行。但是在 ES6 中没有提供类似 Promise.all 这样的方法(为什么?),但我们可以使用 Array.reduce 来实现:
let itemIDs = [1, 2, 3, 4, 5]; itemIDs.reduce((promise, itemID) => { return promise.then(_ => api.deleteItem(itemID)); }, Promise.resolve());
在这种情况下,我们需要等待每次调用 api.deleteItem() 完成之后才能进行下一次调用。这种方法,比为每个 itemID 写 .then() 更简洁更通用:
Promise.resolve() .then(_ => api.deleteItem(1)) .then(_ => api.deleteItem(2)) .then(_ => api.deleteItem(3)) .then(_ => api.deleteItem(4)) .then(_ => api.deleteItem(5));Race
ES6 提供的另一个很方便的函数是 Promise.race。跟 Promise.all 一样,接收一个 Promise 数组,并同时运行它们,但不同的是,会在一旦任何 Promise 完成或失败的情况下返回,并放弃所有其他的结果。
例如,我们可以创建一个在几秒钟之后超时的 Promise:
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(reject, ms); }) } Promise.race([readFilePromise("index.html"), timeout(1000)]) .then(data => console.log(data)) .catch(e => console.log("Timed out after 1 second"))
需要注意的是,其他 Promise 仍将继续运行 ,只是看不到结果而已。
捕获错误捕获错误最常见的方式是添加一个 .catch() 代码块,这将捕获前面所有 .then() 代码块中的错误 :
Promise.resolve() .then(_ => api.getItem(1)) .then(item => { item.amount++; return api.updateItem(1, item); }) .catch(e => { console.log("failed to get or update item"); })
在这里,只要有 getItem 或者 updateItem 失败,catch()就会被触发。但是如果我们想分开处理 getItem 的错误怎么办?只需再插入一个catch() 就可以,它也可以返回另一个 Promise。
Promise.resolve() .then(_ => api.getItem(1)) .catch(e => api.createItem(1, {amount: 0})) .then(item => { item.amount++; return api.updateItem(1, item); }) .catch(e => { console.log("failed to update item"); })
现在,如果getItem()失败,我们通过第一个 catch 介入并创建一条新的记录。
抛出错误应该将 then() 语句中的所有代码视为 try 块内的所有代码。return Promise.reject() 和 throw new Error() 都会导致下一个 catch() 代码块的运行。
这意味着运行时错误也会触发 catch(),所以不要去假设错误的来源。例如,在下面的代码中,我们可能希望该 catch() 只能获得 getItem 抛出的错误,但是如示例所示,它还会在我们的 then() 语句中捕获运行时错误。
api.getItem(1) .then(item => { delete item.owner; console.log(item.owner.name); }) .catch(e => { console.log(e); // Cannot read property "name" of undefined })动态链
有时,我们想要动态地构建 Promise 链,例如,在满足特定条件时,插入一个额外的步骤。在下面的示例中,在读取给定文件之前,我们可以选择创建一个锁定文件:
function readFileAndMaybeLock(filename, createLockFile) { let promise = Promise.resolve(); if (createLockFile) { promise = promise.then(_ => writeFilePromise(filename + ".lock", "")) } return promise.then(_ => readFilePromise(filename)); }
一定要通过重写 promise = promise.then(/*...*/) 来更新 Promise 的值。参看接下来反模式中会提到的 多次调用 then()。
反模式Promise 是一个整洁的抽象,但很容易陷入某些陷阱。以下是我遇到的一些最常见的问题。
重回回调地狱当我第一次从回调函数转到 Promise 时,发现很难摆脱一些旧习惯,仍像使用回调函数一样嵌套 Promise:
api.getItem(1) .then(item => { item.amount++; api.updateItem(1, item) .then(update => { api.deleteItem(1) .then(deletion => { console.log("done!"); }) }) })
这种嵌套是完全没有必要的。有时一两层嵌套可以帮助组合相关任务,但是最好总是使用 .then() 重写成 Promise 垂直链 。
没有返回我遇到的一个经常会犯的错误是在一个 Promise 链中忘记 return 语句。你能发现下面的 bug 吗?
api.getItem(1) .then(item => { item.amount++; api.updateItem(1, item); }) .then(update => { return api.deleteItem(1); }) .then(deletion => { console.log("done!"); })
因为我们没有在第4行的 api.updateItem() 前面写 return,所以 then() 代码块会立即 resolove,导致 api.deleteItem() 可能在 api.updateItem() 完成之前就被调用。
在我看来,这是 ES6 Promise 的一个大问题,往往会引发意想不到的行为。问题是, .then() 可以返回一个值,也可以返回一个新的 Promise,undefined 完全是一个有效的返回值。就个人而言,如果我负责 Promise API,我会在 .then() 返回 undefined 时抛出运行时错误,但现在我们需要特别注意 return 创建的 Promise。
多次调用 .then()根据规范,在同一个 Promise 上多次调用 then() 是完全有效的,并且回调将按照其注册顺序被调用。但是,我并未见过需要这样做的场景,并且在使用返回值和错误处理时可能会产生一些意外行为:
let p = Promise.resolve("a"); p.then(_ => "b"); p.then(result => { console.log(result) // "a" }) let q = Promise.resolve("a"); q = q.then(_ => "b"); q = q.then(result => { console.log(result) // "b" })
在这个例子中,因为我们在每次调用 then() 不更新 p 的值,所以我们看不到 "b" 返回。但是每次调用 then() 时更新 q,所以其行为更可预测。
这也适用于错误处理:
let p = Promise.resolve(); p.then(_ => {throw new Error("whoops!")}) p.then(_ => { console.log("hello!"); // "hello!" }) let q = Promise.resolve(); q = q.then(_ => {throw new Error("whoops!")}) q = q.then(_ => { console.log("hello"); // We never reach here })
在这里,我们期望的是抛出一个错误来打破 Promise 链,但由于没有更新 p 的值,所以第二个 then() 仍会被调用。
有可能在一个 Promise 上多次调用 .then() 有很多理由 ,因为它允许将 Promise 分配到几个新的独立的 Promise 中,但是还没发现真实的使用场景。
混合使用回调和 Promise很容易进入一种陷阱,在使用基于 Promise 库的同时,仍在基于回调的项目中工作。始终避免在 then() 或 catch() 使用回调函数 ,否则 Promise 会吞噬任何后续的错误,将其作为 Promise 链的一部分。例如,以下内容看起来是一个挺合理的方式,使用回调函数来包装一个 Promise:
function getThing(callback) { api.getItem(1) .then(item => callback(null, item)) .catch(e => callback(e)); } getThing(function(err, thing) { if (err) throw err; console.log(thing); })
这里的问题是,如果有错误,我们会收到关于“Unhandled promise rejection”的警告,即使我们添加了一个 catch() 代码块。这是因为,callback() 在 then() 和 catch() 都会被调用,使之成为 Promise 链的一部分。
如果必须使用回调来包装 Promise,可以使用 setTimeout (或者是 NodeJS 中的 process.nextTick)来打破 Promise:
function getThing(callback) { api.getItem(1) .then(item => setTimeout(_ => callback(null, item))) .catch(e => setTimeout(_ => callback(e))); } getThing(function(err, thing) { if (err) throw err; console.log(thing); })不捕获错误
JavaScript 中的错误处理有点奇怪。虽然支持熟悉的 try/catch 范例,但是没有办法强制调用者以 Java 的方式处理错误。然而,使用回调函数,使用所谓的“errbacks”,即第一个参数是一个错误回调变得很常见。这迫使调用者至少承认错误的可能性。例如,fs 库:
fs.readFile("index.html", "utf8", (err, data) => { if (err) throw err; console.log(data); })
使用 Promise,又将很容易忘记需要进行错误处理,特别是对于敏感操作(如文件系统和数据库访问)。目前,如果没有捕获到 reject 的 Promise,将在 NodeJS 中看到非常丑的警告:
(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops! (node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
确保在主要的事件循环中任何 Promise 链的末尾添加 catch() 以避免这种情况。
总结希望这是一篇有用的关于常见 Promise 模式和反模式的概述。如果你想了解更多,这里有一些有用的资源:
Mozilla 的 ES6 Promise 文档
来自 Google 的 Promise 介绍
Dave Atchley 的 ES6 Promise 概述
更多的 Promise 模式和反模式
或者阅读来自 DataFire 团队的内容
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88776.html
摘要:前端日报精选刘海打理指北中的错误处理模式与反模式译图解和译你并不知道中文装饰器让你的代码更简洁众成翻译第期每个程序员第一份工作前应该知道的件事中的不变性众成翻译写的一次小结掘金内部机制探秘和文末附彩蛋和源码前端杂谈开发实战 2017-09-30 前端日报 精选 iPhone X 刘海打理指北React16中的错误处理ES6 Promise:模式与反模式「译」图解 ArrayBuffer...
摘要:回调函数这是异步编程最基本的方法。对象对象是工作组提出的一种规范,目的是为异步编程提供统一接口。诞生后,出现了函数,它将异步编程带入了一个全新的阶段。 更多详情点击http://blog.zhangbing.club/Ja... Javascript 语言的执行环境是单线程的,如果没有异步编程,根本没法用,非卡死不可。 为了解决这个问题,Javascript语言将任务的执行模式分成两种...
摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...
摘要:又譬如在一个多图展示的网页,由于图片过多或图片太大,我们希望图片加载完再一次性显示,而不是东一张西一张陆续显示,这时候也需要用图片一次性加载功能。 基于promise的图片资源一次性加载或者预加载 作者:NEXT卓 场景描述 不是每个网页端的用户都能用得起光纤,不是每张图片都是压缩得很小,有时候我们也想要看高清大图,但是受限于网速有时候场景是这样的:(很明显左边的第一张图片还没出来,其...
摘要:又譬如在一个多图展示的网页,由于图片过多或图片太大,我们希望图片加载完再一次性显示,而不是东一张西一张陆续显示,这时候也需要用图片一次性加载功能。 基于promise的图片资源一次性加载或者预加载 作者:NEXT卓 场景描述 不是每个网页端的用户都能用得起光纤,不是每张图片都是压缩得很小,有时候我们也想要看高清大图,但是受限于网速有时候场景是这样的:(很明显左边的第一张图片还没出来,其...
阅读 3718·2021-10-14 09:43
阅读 3312·2021-08-25 09:38
阅读 611·2019-08-30 15:55
阅读 1346·2019-08-30 13:05
阅读 2241·2019-08-29 16:05
阅读 505·2019-08-29 12:58
阅读 2792·2019-08-29 12:34
阅读 3242·2019-08-26 12:15