promise和javascript(初学者应该看的JavaScript)
这篇文章算是 JavaScript Promises 比较全面的教程,该文介绍了必要的方法,例如 then,catch和finally。此外,还包括处理更复杂的情况,例如与Promise.all并行执行Promise,通过Promise.race 来处理请求超时的情况,Promise 链以及一些最佳实践和常见的陷阱。
1.JavaScript PromisesPromise 是一个允许我们处理异步操作的对象,它是 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⏰
是后者,因为当一个Promise resolved 后,它就不能再被rejected。
一旦你调用一种方法(resolve 或reject),另一种方法就会失效,因为 promise 处于稳定状态。让我们探索一个 promise 的所有不同状态。
1.2 Promise 状态Promise 可以分为四个状态:
- ⏳ Pending:初始状态,异步操作仍在进行中。
- ✅ Fulfilled:操作成功,它调用.then回调,例如.then(onSuccess)。
- ⛔️ Rejected: 操作失败,它调用.catch或.then的第二个参数(如果有)。例如.catch(onError)或.then(..., onError)。
- Settled:这是 promise 的最终状态。promise 已经死亡了,没有别的办法可以解决或拒绝了。.finally方法被调用。
1.3 Promise 实例方法
Promise API 公开了三个主要方法:then,catch和finally。我们逐一配合事例探讨一下。
Promise thenthen方法可以让异步操作成功或失败时得到通知。它包含两个参数,一个用于成功执行,另一个则在发生错误时使用。
promise.then(onSuccess,onError);
你还可以使用catch来处理错误:
promise.then(onSuccess).catch(onError);
then 返回一个新的 Promise ,这样就可以将多个Promise 链接在一起。就像下面的例子一样:
Promise.resolve()
.then(()=>console.log('then#1'))
.then(()=>console.log('then#2'))
.then(()=>console.log('then#3'));
Promise.resolve立即将Promise 视为成功。因此,以下所有内容都将被调用。输出将是
then#1
then#2
then#3
Promise .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)
输出如下:
在这种情况下,可以看到a,b和c上的错误消息。
我们可以使用then函数的第二个参数来处理错误。但是,请注意,catch将不再执行。
Promise.resolve()
.then(a)
.then(b)
.then(c)
.then(d,()=>console.log('cerroredoutbutnobigdeal'))
.catch(console.error)
由于我们正在处理 .then(..., onError)部分的错误,因此未调用catch。d不会被调用。如果要忽略错误并继续执行Promise链,可以在c上添加一个catch。像这样:
Promise.resolve()
.then(a)
.then(b)
.then(()=>c().catch(()=>console.log('errorignored')))
.then(d)
.catch(console.error)
当然,这种过早的捕获错误是不太好的,因为容易在调试过程中忽略一些潜在的问题。
Promise finallyfinally方法只在 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'));
我们可以直接使用 Promise 对象中四种静态方法。
- Promise.all
- Promise.reject
- Promise.resolve
- Promise.race
这两个是帮助函数,可以让 Promise 立即解决或拒绝。可以传递一个参数,作为下次 .then 的接收:
Promise.resolve('Yay!!!')
.then(console.log)
.catch(console.error)
上面会输出 Yay!!!
Promise.reject('Oops')
.then(console.log)
.catch(console.error)
通常,Promise 是一个接一个地依次执行的,但是你也可以并行使用它们。
假设是从两个不同的api中轮询数据。如果它们不相关,我们可以使用Promise.all()同时触发这两个请求。
在此示例中,主要功能是将美元转换为欧元,我们有两个独立的 API 调用。一种用于BTC/USD,另一种用于获得EUR/USD。如你所料,两个 API 调用都可以并行调用。但是,我们需要一种方法来知道何时同时完成最终价格的计算。我们可以使用Promise.all,它通常在启动多个异步任务并发运行并为其结果创建承诺之后使用,以便人们可以等待所有任务完成。
constaxios=require('axios');
constbitcoinPromise=axios.get('https://api.coinpaprika.com/v1/coins/btc-bitcoin/markets');
constdollarPromise=axios.get('https://api.exchangeratesapi.io/latest?base=USD');
constcurrency='EUR';
//Getthepriceofbitcoinson
Promise.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秒?
这个留给你们自己验证咯。
Promise racePromise.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('http://slowwly.robertomurray.co.uk/delay/3000/url/https://api.jsonbin.io/b/5d1fb4dd138da811182c69af'),
newPromise((resolve,reject)=>setTimeout(()=>reject(newError('requesttimeout')),1000))
])
.then(console.log)
.catch(console.error);
如果请求足够快,那么就会得到请求的结果。
1.5 Promise 常见问题串行执行 promise 并传递参数
这次,我们将对Node的fs使用promises API,并将两个文件连接起来:
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文件。如你所见,writeFile promise返回文件的内容,你可以在下一个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.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));
输出应该是这样的:
以上代码将并发限制为并行执行的3个任务。
实现promiseAllThrottled 一种方法是使用Promise.race来限制给定时间的活动任务数量。
/**
*SimilartoPromise.allbutaconcurrencylimit
*
*@param{Array}iterableArrayoffunctionsthatreturnsapromise
*@param{Object}concurrencymaxnumberofparallelpromisesrunning
*/
functionpromiseAllThrottled(iterable,{concurrency=3}={}){
constpromises=[];
functionenqueue(current=0,queue=[]){
//returnifdone
if(current===iterable.length){returnPromise.resolve();}
//takeonepromisefromcollection
constpromise=iterable[current];
constactivatedPromise=promise();
//addpromisetothefinalresultarray
promises.push(activatedPromise);
//addcurrentactivatedpromisetoqueueandremoveitwhendone
constautoRemovePromise=activatedPromise.then(()=>{
//removepromisefromthequeuewhendone
returnqueue.splice(queue.indexOf(autoRemovePromise),1);
});
//addpromisetothequeue
queue.push(autoRemovePromise);
//ifqueuelength>=concurrency,waitforonepromisetofinishbeforeaddingmore.
constreadyForMore=queue.length<concurrency?Promise.resolve():Promise.race(queue);
returnreadyForMore.then(()=>enqueue(current 1,queue));
}
returnenqueue()
.then(()=>Promise.all(promises));
}
promiseAllThrottled一对一地处理 Promises 。它执行Promises并将其添加到队列中。如果队列小于并发限制,它将继续添加到队列中。达到限制后,我们使用Promise.race等待一个承诺完成,因此可以将其替换为新的承诺。这里的技巧是,promise 自动完成后会自动从队列中删除。另外,我们使用 race 来检测promise 何时完成,并添加新的 promise 。
人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。
作者:Adrian Mejia 译者:前端小智 来源:adrianmjia
原文:https://adrianmejia.com/promises-tutorial-concurrency-in-javascript-node/
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com