初学者应该看的JavaScript Promise 完整指南

这篇文章算是JavaScriptPromises比较全面的教程 , 该文介绍了必要的方法 , 例如then , catch和finally 。 此外 , 还包括处理更复杂的情况 , 例如与Promise.all并行执行Promise , 通过Promise.race来处理请求超时的情况 , Promise链以及一些最佳实践和常见的陷阱 。
1.JavaScriptPromisesPromise是一个允许我们处理异步操作的对象 , 它是es5早期回调的替代方法 。
与回调相比 , Promise具有许多优点 , 例如:
让异步代码更易于阅读 。 提供组合错误处理 。 *更好的流程控制 , 可以让异步并行或串行执行 。回调更容易形成深度嵌套的结构(也称为回调地狱) 。 如下所示:
a(()=>{b(()=>{c(()=>{d(()=>{//andsoon...});});});});如果将这些函数转换为Promise , 则可以将它们链接起来以生成更可维护的代码 。 像这样:
Promise.resolve().then(a).then(b).then(c).then(d).catch(console.error);在上面的示例中 , Promise对象公开了.then和.catch方法 , 我们稍后将探讨这些方法 。
1.1如何将现有的回调API转换为Promise?我们可以使用Promise构造函数将回调转换为Promise 。
Promise构造函数接受一个回调 , 带有两个参数resolve和reject 。
Resolve:是在异步操作完成时应调用的回调 。 Reject:是发生错误时要调用的回调函数 。构造函数立即返回一个对象 , 即Promise实例 。 当在promise实例中使用.then方法时 , 可以在Promise“完成”时得到通知 。 让我们来看一个例子 。
Promise仅仅只是回调?并不是 。 承诺不仅仅是回调 , 但它们确实对.then和.catch方法使用了异步回调 。 Promise是回调之上的抽象 , 我们可以链接多个异步操作并更优雅地处理错误 。 来看看它的实际效果 。
Promise反面模式(Promises地狱)a(()=>{b(()=>{c(()=>{d(()=>{//andsoon...});});});});不要将上面的回调转成下面的Promise形式:
a().then(()=>{returnb().then(()=>{returnc().then(()=>{returnd().then(()=>{//??Pleasenevereverdotothis!??});});});});上面的转成 , 也形成了Promise地狱 , 千万不要这么转 。 相反 , 下面这样做会好点:
a().then(b).then(c).then(d)超时你认为以下程序的输出的是什么?
constpromise=newPromise((resolve,reject)=>{setTimeout(()=>{resolve('timeisup?');},1e3);setTimeout(()=>{reject('Oops');},2e3);});promise.then(console.log).catch(console.error);是输出:
timeisup?Oops!还是输出:
timeisup?是后者 , 因为当一个Promiseresolved后 , 它就不能再被rejected 。
一旦你调用一种方法(resolve或reject) , 另一种方法就会失效 , 因为promise处于稳定状态 。 让我们探索一个promise的所有不同状态 。
1.2Promise状态Promise可以分为四个状态:
?Pending:初始状态 , 异步操作仍在进行中 。 ?Fulfilled:操作成功 , 它调用.then回调 , 例如.then(onSuccess) 。 ??Rejected:操作失败 , 它调用.catch或.then的第二个参数(如果有) 。 例如.catch(onError)或.then(...,onError) 。 Settled:这是promise的最终状态 。 promise已经死亡了 , 没有别的办法可以解决或拒绝了 。 .finally方法被调用 。 Promisethenthen方法可以让异步操作成功或失败时得到通知 。 它包含两个参数 , 一个用于成功执行 , 另一个则在发生错误时使用 。
promise.then(onSuccess,onError);你还可以使用catch来处理错误:
promise.then(onSuccess).catch(onError);Promise链then返回一个新的Promise , 这样就可以将多个Promise链接在一起 。 就像下面的例子一样:
Promise.resolve().then(()=>console.log('then#1')).then(()=>console.log('then#2')).then(()=>console.log('then#3'));Promise.resolve立即将Promise视为成功 。 因此 , 以下所有内容都将被调用 。 输出将是
then#1then#2then#3PromisecatchPromise.catch方法将函数作为参数处理错误 。 如果没有出错 , 则永远不会调用catch方法 。
假设我们有以下承诺:1秒后解析或拒绝并打印出它们的字母 。
consta=()=>newPromise((resolve)=>setTimeout(()=>{console.log('a'),resolve()},1e3));constb=()=>newPromise((resolve)=>setTimeout(()=>{console.log('b'),resolve()},1e3));constc=()=>newPromise((resolve,reject)=>setTimeout(()=>{console.log('c'),reject('Oops!')},1e3));constd=()=>newPromise((resolve)=>setTimeout(()=>{console.log('d'),resolve()},1e3));请注意 , c使用reject('Oops!')模拟了拒绝 。
Promise.resolve().then(a).then(b).then(c).then(d).catch(console.error)输出如下:
我们可以使用then函数的第二个参数来处理错误 。 但是 , 请注意 , catch将不再执行 。
Promise.resolve().then(a).then(b).then(c).then(d,()=>console.log('cerroredoutbutnobigdeal')).catch(console.error) Promise.resolve().then(a).then(b).then(()=>c().catch(()=>console.log('errorignored'))).then(d).catch(console.error) Promisefinallyfinally方法只在Promise状态是settled时才会调用 。
如果你希望一段代码即使出现错误始终都需要执行 , 那么可以在.catch之后使用.then 。
Promise.resolve().then(a).then(b).then(c).then(d).catch(console.error).then(()=>console.log('alwayscalled'));或者可以使用.finally关键字:
Promise.resolve().then(a).then(b).then(c).then(d).catch(console.error).finally(()=>console.log('alwayscalled'));1.4Promise类方法我们可以直接使用Promise对象中四种静态方法 。
Promise.allPromise.rejectPromise.resolvePromise.racePromise.resolve和Promise.reject这两个是帮助函数 , 可以让Promise立即解决或拒绝 。 可以传递一个参数 , 作为下次.then的接收:
Promise.resolve('Yay!!!').then(console.log).catch(console.error)上面会输出Yay!!!
Promise.reject('Oops').then(console.log).catch(console.error)使用Promise.all并行执行多个Promise通常 , Promise是一个接一个地依次执行的 , 但是你也可以并行使用它们 。
假设是从两个不同的api中轮询数据 。 如果它们不相关 , 我们可以使用Promise.all()同时触发这两个请求 。
在此示例中 , 主要功能是将美元转换为欧元 , 我们有两个独立的API调用 。 一种用于BTC/USD , 另一种用于获得EUR/USD 。 如你所料 , 两个API调用都可以并行调用 。 但是 , 我们需要一种方法来知道何时同时完成最终价格的计算 。 我们可以使用Promise.all , 它通常在启动多个异步任务并发运行并为其结果创建承诺之后使用 , 以便人们可以等待所有任务完成 。
constaxios=require('axios');constbitcoinPromise=axios.get('');constdollarPromise=axios.get('');constcurrency='EUR';//GetthepriceofbitcoinsonPromise.all([bitcoinPromise,dollarPromise]).then(([bitcoinMarkets,dollarExchanges])=>{constbyCoinbaseBtc=d=>d.exchange_id==='coinbase-pro'&&d.pair==='BTC/USD';constcoinbaseBtc=bitcoinMarkets.data.find(byCoinbaseBtc)constcoinbaseBtcInUsd=coinbaseBtc.quotes.USD.price;constrate=dollarExchanges.data.rates[currency];returnrate*coinbaseBtcInUsd;}).then(price=>console.log(`TheBitcoinin${currency}is${price.toLocaleString()}`)).catch(console.log)如你所见 , Promise.all接受了一系列的Promises 。 当两个请求的请求都完成后 , 我们就可以计算价格了 。
我们再举一个例子:
consta=()=>newPromise((resolve)=>setTimeout(()=>resolve('a'),2000));constb=()=>newPromise((resolve)=>setTimeout(()=>resolve('b'),1000));constc=()=>newPromise((resolve)=>setTimeout(()=>resolve('c'),1000));constd=()=>newPromise((resolve)=>setTimeout(()=>resolve('d'),1000));console.time('promise.all');Promise.all([a(),b(),c(),d()]).then(results=>console.log(`Done!${results}`)).catch(console.error).finally(()=>console.timeEnd('promise.all'));解决这些Promise要花多长时间?5秒?1秒?还是2秒?
这个留给你们自己验证咯 。
PromiseracePromise.race(iterable)方法返回一个promise , 一旦迭代器中的某个promise解决或拒绝 , 返回的promise就会解决或拒绝 。
consta=()=>newPromise((resolve)=>setTimeout(()=>resolve('a'),2000));constb=()=>newPromise((resolve)=>setTimeout(()=>resolve('b'),1000));constc=()=>newPromise((resolve)=>setTimeout(()=>resolve('c'),1000));constd=()=>newPromise((resolve)=>setTimeout(()=>resolve('d'),1000));console.time('promise.race');Promise.race([a(),b(),c(),d()]).then(results=>console.log(`Done!${results}`)).catch(console.error).finally(()=>console.timeEnd('promise.race'));输出是什么?
输出b 。 使用Promise.race , 最先执行完成就会结果最后的返回结果 。
你可能会问:Promise.race的用途是什么?
我没胡经常使用它 。 但是 , 在某些情况下 , 它可以派上用场 , 比如计时请求或批量处理请求数组 。
Promise.race([fetch('://api.jsonbin.io/b/5d1fb4dd138da811182c69af'),newPromise((resolve,reject)=>setTimeout(()=>reject(newError('requesttimeout')),1000))]).then(console.log).catch(console.error); constfs=require('fs').promises;//requiresnodev8+fs.readFile('file.txt','utf8').then(content1=>fs.writeFile('output.txt',content1)).then(()=>fs.readFile('file2.txt','utf8')).then(content2=>fs.writeFile('output.txt',content2,{flag:'a+'})).catch(error=>console.log(error));在此示例中 , 我们读取文件1并将其写入output文件 。 稍后 , 我们读取文件2并将其再次附加到output文件 。 如你所见 , writeFilepromise返回文件的内容 , 你可以在下一个then子句中使用它 。
如何链接多个条件承诺?你可能想要跳过Promise链上的特定步骤 。 有两种方法可以做到这一点 。
consta=()=>newPromise((resolve)=>setTimeout(()=>{console.log('a'),resolve()},1e3));constb=()=>newPromise((resolve)=>setTimeout(()=>{console.log('b'),resolve()},2e3));constc=()=>newPromise((resolve)=>setTimeout(()=>{console.log('c'),resolve()},3e3));constd=()=>newPromise((resolve)=>setTimeout(()=>{console.log('d'),resolve()},4e3));constshouldExecA=true;constshouldExecB=false;constshouldExecC=false;constshouldExecD=true;Promise.resolve().then(()=>shouldExecA&&a()).then(()=>shouldExecB&&b()).then(()=>shouldExecC&&c()).then(()=>shouldExecD&&d()).then(()=>console.log('done'))如果你运行该代码示例 , 你会注意到只有a和d被按预期执行 。
另一种方法是创建一个链 , 然后仅在以下情况下添加它们:
constchain=Promise.resolve();if(shouldExecA)chain=chain.then(a);if(shouldExecB)chain=chain.then(b);if(shouldExecC)chain=chain.then(c);if(shouldExecD)chain=chain.then(d);chain.then(()=>console.log('done'));如何限制并行Promise?要做到这一点 , 我们需要以某种方式限制Promise.all 。
假设你有许多并发请求要执行 。 如果使用Promise.all是不好的(特别是在API受到速率限制时) 。 因此 , 我们需要一个方法来限制Promise个数 , 我们称其为promiseAllThrottled 。
//simulate10asynctasksthattakes5secondstocomplete.constrequests=Array(10).fill().map((_,i)=>()=>newPromise((resolve=>setTimeout(()=>{console.log(`exec'ingtask#${i}`),resolve(`task#${i}`);},5000))));promiseAllThrottled(requests,{concurrency:3}).then(console.log).catch(error=>console.error('Oopssomethingwentwrong',error));输出应该是这样的:
实现promiseAllThrottled一种方法是使用Promise.race来限制给定时间的活动任务数量 。
/***SimilartoPromise.allbutaconcurrencylimit**@param{Array}iterableArrayoffunctionsthatreturnsapromise*@param{Object}concurrencymaxnumberofparallelpromisesrunning*/functionpromiseAllThrottled(iterable,{concurrency=3}={}){constpromises=[];functionenqueue(current=0,queue=[]){//returnifdoneif(current===iterable.length){returnPromise.resolve();}//takeonepromisefromcollectionconstpromise=iterable[current];constactivatedPromise=promise();//addpromisetothefinalresultarraypromises.push(activatedPromise);//addcurrentactivatedpromisetoqueueandremoveitwhendoneconstautoRemovePromise=activatedPromise.then(()=>{//removepromisefromthequeuewhendonereturnqueue.splice(queue.indexOf(autoRemovePromise),1);});//addpromisetothequeuequeue.push(autoRemovePromise);//ifqueuelength>=concurrency,waitforonepromisetofinishbeforeaddingmore.constreadyForMore=queue.lengthenqueue(current+1,queue));}returnenqueue().then(()=>Promise.all(promises));}promiseAllThrottled一对一地处理Promises 。 它执行Promises并将其添加到队列中 。 如果队列小于并发限制 , 它将继续添加到队列中 。 达到限制后 , 我们使用Promise.race等待一个承诺完成 , 因此可以将其替换为新的承诺 。 这里的技巧是 , promise自动完成后会自动从队列中删除 。 另外 , 我们使用race来检测promise何时完成 , 并添加新的promise 。
人才们的【三连】就是小智不断分享的最大动力 , 如果本篇博客有任何错误和建议 , 欢迎人才们留言 , 最后 , 谢谢大家的观看 。
作者:AdrianMejia译者:前端小智来源:adrianmjia
【初学者应该看的JavaScript Promise 完整指南】原文:


    推荐阅读