原文:ES6Promises:PatternsandAnti-Patterns作者:BobbyBrennan几年前刚开始接触NodeJS的时候,我对现在所谓的“回调地狱”印象深刻.困扰。幸好现在是2017年了,NodeJS采用了很多JavaScript的最新特性,从v4开始就支持Promise了。虽然Promises可以让代码更加简洁易读,但对于只熟悉回调函数的人来说,可能还是会持怀疑态度。在这里,我将列出我在使用Promise时学到的一些基本模式,以及我踩过??的一些陷阱。注意:本文将使用箭头函数。如果您不熟悉它们,它们实际上非常简单。建议阅读使用它们的好处。模式和最佳实践使用Promise如果你使用的是已经支持Promise的第三方库,那么使用听起来很简单。只有两个函数需要关心:then()和catch()。例如,有一个客户端API具有三个方法,getItem()、updateItem()和deleteItem(),每个方法都返回一个Promise:Promise.resolve().then(_=>{returnapi.getItem(1)}).then(item=>{item.amount++returnapi.updateItem(1,item);}).then(update=>{returnapi.deleteItem(1);}).catch(e=>{console.log('errorwhileworkingonitem1');})每次调用then()都会在Promise链中创建一个新步骤,如果链中任何地方发生错误,都会触发下一个catch()。then()和catch()都可以返回一个值或一个新的Promise,结果将传递给Promise链中的下一个then()。为了对比,这里使用回调函数实现相同的逻辑:api.getItem(1,(err,data)=>{if(err)throwerr;item.amount++;api.updateItem(1,item,(err,update)=>{if(err)throwerr;api.deleteItem(1,(err)=>{if(err)throwerr;})})})第一个区别是我们有回调函数在流程的每一步都进行错误处理,而不是一个包罗万象。回调函数的第二个问题比较直观,每一步都要水平缩进,使用Promise的代码有明显的顺序关系。承诺回调函数要学习的第一个技巧是如何将回调函数转换为承诺。您可能正在使用仍然基于回调的库,或者您自己的遗留代码,但不要担心,因为只需要几行代码就可以将其包装到Promise中。下面是将Node中的回调方法fs.readFile转换为Promise的示例:=>{if(err)reject(err);elseresolve(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的函数:}if(filename==='index.html'){返回承诺。解决('
你好!
');}returnnewPromise((resolve,reject)=>{/*...*/})}请注意,虽然可以将任何内容(或没有值)传递给Promise.reject(),但最好传递一个错误。并行运行Promise.all是一种并行运行Promise数组的方法,即同时运行。例如,我们有一个要从磁盘读取的文件列表。使用上面创建的readFilePromise函数,它看起来像这样:files=>{console.log('index:',files[0]);console.log('blog:',files[1]);console.log('terms:',files[2]);})我什至不会尝试使用传统的回调函数编写等价物,那样会很混乱且容易出错。串行运行有时会出现问题,同时运行一堆Promise。例如,如果您尝试使用Promise.allAPI来检索一堆资源,您可能会在达到速率限制时开始响应429错误。一种解决方案是连续运行Promises,或者一个接一个地运行。但是ES6中没有类似Promise.all的方法(为什么?),但是我们可以使用Array.reduce来实现:letitemIDs=[1,2,3,4,5];itemIDs.reduce((promise,itemID)=>{returnpromise.then(_=>api.deleteItem(itemID));},Promise.resolve());在这种情况下,我们需要等待对api.deleteItem()的每次调用完成,然后再进行下一次调用。这种方法比为每个itemID编写.then()更简洁、更通用:Promise.resolve().then(_=>api.deleteItem(1)).then(_=>api.deleteItem(2))。然后(_=>api.deleteItem(3)).then(_=>api.deleteItem(4)).then(_=>api.deleteItem(5));RaceES6提供的另一个方便的功能是Promise。种族。与Promise.all一样,接受一组Promise并同时运行它们,但一旦任何Promise完成或失败就返回,并丢弃所有其他结果。例如,我们可以创建一个几秒后超时的Promise:functiontimeout(ms){returnnewPromise((resolve,reject)=>{setTimeout(reject,ms);})}Promise.race([readFilePromise('index.html'),timeout(1000)]).then(data=>console.log(data)).catch(e=>console.log("1秒后超时"))注意,其他承诺仍然会运行,他们只是看不到他们的结果。捕获错误的最常见方法是添加一个.catch()块,它将捕获所有前面的.then()块中的错误:Promise.resolve().then(_=>api.getItem(1)).then(item=>{item.amount++;returnapi.updateItem(1,item);}).catch(e=>{console.log('failedtogetorupdateitem');})在这里,catch()将每当getItem或updateItem失败时触发。但是如果我们想单独处理getItem错误怎么办?只需插入另一个catch(),它也可以返回另一个Promise。Promise.resolve().then(_=>api.getItem(1)).catch(e=>api.createItem(1,{amount:0})).then(item=>{item.amount++;返回api.updateItem(1,item);}).catch(e=>{console.log('failedtoupdateitem');})现在,如果getItem()失败,我们介入第一个catch并创建一个新的的记录。抛出错误应该将then()语句中的所有代码视为在try块中。两者都返回Promise.reject()并抛出newError()导致下一个catch()块运行。这意味着运行时错误也会触发catch(),所以不要假设错误的来源。例如,在下面的代码中,我们可能希望catch()只获取getItem抛出的错误,但如示例所示,它还会捕获then()语句中的运行时错误。api.getItem(1).then(item=>{deleteitem.owner;console.log(item.owner.name);}).catch(e=>{console.log(e);//无法读取属性'name'ofundefined})动态链有时,我们希望动态构建Promise链,例如,在满足特定条件时插入一个额外的步骤。在下面的示例中,我们可以选择在读取给定文件之前创建一个锁定文件:functionreadFileAndMaybeLock(filename,createLockFile){letpromise=Promise.resolve();if(createLockFile){promise=promise.then(_=>writeFilePromise(filename+'.lock',''))}returnpromise.then(_=>readFilePromise(filename));}一定要重写promise=promise.then(/*...*/)来更新Promise的值。请参阅下面的反模式中多次调用then()。反模式Promise是一个简洁的抽象,但很容易陷入某些陷阱。以下是我遇到的一些最常见的问题。回到回调地狱当我第一次从回调转向Promises时,我发现很难摆脱一些旧习惯,仍然像使用回调一样嵌套Promises:api.getItem(1).then(item=>{item.amount++;api.updateItem(1,item).then(update=>{api.deleteItem(1).then(deletion=>{console.log('done!');})})})这种嵌套是完全没有必要的.有时一两层嵌套可以帮助对相关任务进行分组,但最好始终使用.then()重写为Promise垂直链。不返回我遇到的一个常见错误是忘记Promise链中的返回语句。你能发现下面的错误吗?api.getItem(1).then(item=>{item.amount++;api.updateItem(1,item);}).then(update=>{returnapi.deleteItem(1);}).then(deletion=>{console.log('done!');})因为我们在第4行没有在api.updateItem()前面写return,所以then()代码块会立即resolove,导致api.deleteItem()可能被在.updateItem()完成之前调用的api中。在我看来,这是ES6Promises的一个大问题,它往往会导致意外行为。问题是,.then()可以返回一个值,或者它可以返回一个新的Promise,而undefined根本就是一个有效的返回值。就个人而言,如果我负责PromiseAPI,如果.then()返回undefined,我会抛出运行时错误,但现在我们需要特别注意返回创建的Promise。多次调用.then()根据规范,在同一个Promise上多次调用then()是完全有效的,并且回调将按照它们注册的顺序被调用。但是,我还没有看到需要这样做的场景,并且在使用返回值和错误处理时可能会出现一些意外行为:letp=Promise.resolve('a');p.then(_=>'b');p.then(result=>{console.log(result)//'a'})letq=Promise.resolve('a');q=q.then(_=>'b');q=q.then(result=>{console.log(result)//'b'})在这个例子中,因为我们没有在每次调用then()时更新p的值,所以我们看不到'b''返回。但是每次then()被调用时q都会更新,所以它的行为更容易预测。这也适用于错误处理:letp=Promise.resolve();p.then(_=>{thrownewError("whoops!")})p.then(_=>{console.log('hello!');//'hello!'})letq=Promise.resolve();q=q.then(_=>{thrownewError("whoops!")})q=q.then(_=>{console.log('hello');//我们从来没有到达这里})这里我们期望一个错误来破坏Promise链,但是由于p的值没有更新,第二个then()仍然会被调用。有很多原因可以在Promise上多次调用.then(),因为它允许将Promise拆分为几个新的独立Promise,但尚未找到真正的用例。很容易陷入混合回调和Promises的陷阱,使用基于Promise的库,同时仍在基于回调的项目中工作。始终避免在then()或catch()中使用回调函数否则Promise会将任何后续错误作为Promise链的一部分。例如,以下似乎是用回调函数包装Promise的合理方法:functiongetThing(callback){api.getItem(1).then(item=>callback(null,item)).catch(e=>callback(e));}getThing(function(err,thing){if(err)throwerr;console.log(thing);})这里的问题是,如果有错误,我们会收到一条关于“未处理的承诺拒绝”警告,即使我们添加了一个catch()块。这是因为在then()和catch()中都调用了callback(),使其成为Promise链的一部分。如果必须用回调包装Promise,可以使用setTimeout(或NodeJS中的process.nextTick)来打破Promise:functiongetThing(callback){api.getItem(1).then(item=>setTimeout(_=>callback(null,item))).catch(e=>setTimeout(_=>callback(e)));}getThing(function(err,thing){if(err)throwerr;console.log(thing);})JavaScript中的错误处理有点奇怪。虽然支持熟悉的try/catch范例,但无法强制调用方以Java方式处理错误。然而,对于回调函数,使用所谓的“errbacks”变得很普遍,其中第一个参数是错误回调。这迫使调用者至少承认错误的可能性。例如,fs库:fs.readFile('index.html','utf8',(err,data)=>{if(err)throwerr;console.log(data);})使用Promise,它将轻松忘记错误处理的需要,尤其是对于文件系统和数据库访问等敏感操作。当前,如果未捕获被拒绝的Promise,您将在NodeJS中看到一个非常难看的警告:(node:29916)UnhandledPromiseRejectionWarning:UnhandledPromiserejection(rejectionid:1):Error:whoops!(节点:29916)弃用警告:不推荐使用未处理的承诺拒绝。将来,未处理的承诺拒绝将以非零退出代码终止Node.js进程。确保在主事件循环中任何Promise链的末尾添加catch()以避免出现这种情况。结论希望这是对常见Promise模式和反模式的有用概述。如果您想了解更多信息,这里有一些有用的资源:Mozilla的ES6Promise文档,来自Google的PromiseIntroductionDaveAtchley的ES6Promise概述,了解更多Promise模式和反模式,或者阅读DataFire团队的文章