当前位置: 首页 > 科技观察

JavaScript异步编程指南—给我一个承诺

时间:2023-03-11 21:24:26 科技观察

转载请联系MayJun公众号。“给我一个承诺,我哪儿也不去,就在这里等你。”,我会在文章中提到它!随着ES6标准的出现,给我们带来了新的异步解决方案Promise。目前,无论是在浏览器端还是Node.js服务器端,JavaScript中大部分新的异步API都是基于Promise构建的。以前也有基于Callback的解决方案,将它们转化为Promise。看完笔者上一节《Promise前世Deferred的解释》,本章学习起来会更加轻松。在开始之前,我们先了解一下Promise/A+规范,以便更好地理解Promise的一些思想和规则。了解Promise是什么?Promise是用来表示异步操作结果的对象。我们没有办法同步知道它的结果,但是这个结果可以用来代表一个未来的值,我们可以在未来的某个时间点得到这个值,它可能会成功,可能会失败,并且会永远等待(参见下面的“不可撤销的承诺”)。PromiseA+规范中有一些专业术语,我们先了解一下:fulfill:Promise成功时的一个结果,表示resolution。在许多Promise实现中,将使用resolve代替。这是一个意思。通常在resolve中,我们会收到程序的正确响应。reject:Promise失败时的结果,通常在reject中我们会收到一条错误消息。eventualvalue:表示最终的值,也就是Promiseresolve时传递给resolutioncallback的值,比如resolve(value),此时Promise状态就会结束,进入fulfill。reason:拒绝原因,指Promise被拒绝时传递给rejection回调的值,例如reject(reason),此时Promise状态结束,进入reject。当我们想要更深入地理解Promise时,这些概念是需要的。比如最好按照这个规范来实现我们自己的Promise。本节主要使用Promise的基础知识,后面也会进行异步推进。下面说说Promise的实现原理,实现一个自己的Promise对象。Promise状态流程Promise在创建时处于等待状态。最后,要么成功,要么失败。和状态机一样,从这张图也可以看出状态只能从Pending->Fulfilled或Pending->Rejected改变,将Callback转化为Promise。目前有一些API是直接基于Promise的,比如FetchAPI从网络获取数据。fetch('http://example.com/movies.json').then(函数(响应){//TODO});举个Node.js的例子,比如fs.readFile()这个API默认是callback的形式,转成Promise也很方便。fs.readFile('xxx',function(err,result){console.error('Error:',err);console.log('Result:',result);});方法一:newPromiseimplementsnewPromise()就是创建一个新的Promise对象,然后我们可以使用resolve和reject来返回结果信息。constreadFile=filename=>newPromise((resolve,reject)=>{fs.readFile(filename,(err,file)=>{if(err){reject(err);}else{resolve(file);}})});readFile('xxx').then(result=>console.log(result)).catch(err=>console.log(err));方法二:Node.jsutil.promisify工具Node.jsutil该模块提供了许多实用功能。为了解决回调地狱的问题,Nodejsv8.0.0提供了promisify方法将Callback转换为Promise对象。作者在《Node.js源码分析util.promisify如何将Callback转为Promise》之前也写过分析const{promisify}=require('util');constreadFilePromisify=util.promisify(fs.readFile);//转PromisereadFilePromisify('xxx').then(result=>console.log(result)).catch(err=>console.log(err));此外,Node.jsfs模块的fs.promisesAPI提供了一组备用的异步文件系统方法,这些方法返回Promise对象而不是使用回调。API可通过require('fs').promises或require('fs/promises')访问。constfsPromises=require('fs/promises');fsPromises('xxx').then(result=>console.log(result)).catch(err=>console.log(err));Promise错误管理Promise实例提供了两种捕获错误的方式:一种是传入第二个参数给Promise.then()方法,另一种是Promise实例的catch()方法。.then()第二个回调参数根据就近原则捕获错误,不会影响后续then的进行。Promise抛出错误是有冒泡机制的,可以不断传递,可以用catch()统一处理。constajax=function(){console.log('promise开始执行');returnnewPromise(function(resolve,reject){setTimeout(function(){reject(`There'samistake`);},1000);});}ajax().then(function(){console.log('then1');returnPromise.resolve();},err=>{console.log('then1captureserr:',err);})。then(function(){console.log('then2');returnPromise.reject(`There'sathhenmitake`);}).catch(err=>{console.log('catchcapturederr:',err);});//输出//promise开始执行//then1:There'samistake捕获的err//then2//catch捕获的err:There'sathenmistakePromise几种方法Promise.all()并行执行Promise.all()接收数组形式的多个Promise实例。就像一个for循环,执行传入的多个Promise实例,当所有结果都成功后,返回结果。一旦其中一个Promise实例在执行过程中被拒绝,就会触发Promise.all()的catch()函数。以下示例加载3张图片,如果全部成功,则将结果渲染到页面。functionloadImg(src){returnnewPromise((resolve,reject)=>{letimg=document.createElement('img');img.src=src;img.onload=()=>{resolve(img);}img.onerror=(err)=>{reject(err)}})}functionshowImgs(imgs){imgs.forEach(function(img){document.body.appendChild(img)})}Promise.all([loadImg('http://www.xxxxxx.com/uploads/1.jpg'),loadImg('http://www.xxxxxx.com/uploads/2.jpg'),loadImg('http://www.xxxxxx.com/uploads/3.jpg')]).then(showImgs)在Promise链调用中,任何时候只执行一个任务,只有在这个任务完成后才能执行下一个任务。如果我有两个或多个任务之间没有顺序依赖,希望它们可以并行执行,这样可以提高效率。这时候可以选择Promise.all()。Promise.race()是最先执行Promise.race()的,只要有一个Promise实例先发生变化,其他的就不会响应。这就像一场短跑比赛,我只想知道谁是第一名,当第一个人冲过终点线时,我的目标就达到了。还是基于上面的例子,只要加载了一张图片,就会直接添加到页面中。函数showImgs(img){letp=document.createElement('p');p.appendChild(img);document.body.appendChild(p);}Promise.race([loadImg('http://www.xxxxxx.com/uploads/1.jpg'),loadImg('http://www.xxxxxx.com/uploads/2.jpg'),loadImg('http://www.xxxxxx.com/uploads/3.jpg')]).then(showImgs)Promise.finally()执行后,Promise的最终结果要么在then中成功,要么在catch中失败。也许在某些时候我们需要一个可以随时调用的回调来做一些清理工作。ES7的新增finally或许是你不错的选择。Promise.finally()从Node.js版本10.3.0开始支持。constp=Promise.resolve();p.then(onSuccess).catch(onFailed).finally(cleanup);Promise.any()Promise.any()接受一个数组作为参数,可以传入多个Promise实例,只要当其中一个Promise变为Fulfilled时,就会返回Promise实例。只有当所有Promise实例都处于Rejected状态时,Promise.any()才会返回Rejected。Promise.any()自Node.js版本15.14.0起受支持。Promise.any([Promise.reject('FAILED'),Promise.resolve('SUCCESS1'),Promise.resolve('SUCCESS2'),]).then(result=>{console.log(result);//成功1});Promise.allSettled()Promise.allSettled()类似于Promise.all(),不同的是Promise.allSettled()执行后不会失败,它会以数组的形式返回所有的结果,我们可以获取每个Promise实例的执行状态和结果。Promise.allSettled()自Node.js版本12.10.0起受支持。Promise.allSettled([Promise.reject('FAILED'),Promise.resolve('SUCCESS1'),Promise.resolve('SUCCESS2'),]).then(results=>{console.log(results);});//[//{status:'rejected',reason:'FAILED'},//{status:'fulfilled',value:'SUCCESS1'},//{status:'fulfilled',value:'SUCCESS2'}//]无法取消的承诺,开头是一句“给我一个承诺,我哪儿也不去,就站在这里等你。”就像男生对喜欢的女生说:“给我一个承诺,我哪儿也不去,就在原地等你。”比如我们的程序创建了一个Promise对象promise,并为它注册了completion和rejectionhandlers,由于某些原因,我们没有给它resolve/reject,这时候promise对象会一直处于Pending等待状态。我们也外部不能cancel,如果之后还有业务要处理,就一直等下去,我们自己包装一个Promise对象的时候,尽量避免这种情况的发生。constpromise=newPromise((resolve,reject)=>{//Noresolveandnoreject});console.log(promise);//Promise{}promise.then(()=>{console.log('resolve');}).catch(err=>{console.log('reject');});使用Promise改造Callback回调地狱示例这是我们在《JavaScript异步编程指南》回调部分写的示例:fs.readdir('/path/xxxx',(err,files)=>{if(err){//TODO...}files.forEach((filename,index)=>{fs.lstat(filename,(err,stats)=>{if(err){//TODO...}if(stats.isFile()){fs.readFile(文件名,(err,file)=>{//TODO})}})})});Node.js的fs模块为我们提供了promises对象,现在解决了深度嵌套的问题。这个问题还有更优雅的写法,我们会在后面的Async/Await章节继续介绍。constfs=require('fs').promises;constpath=require('path');constrootDir='/path/xxxx';fs.readdir('/path/xxxx').then(files=>{files.forEach(checkFileAndRead);}).catch(err=>{//TODO});functioncheckFileAndRead(filename,index){constfile=path.resolve(rootDir,filename);returnfs.lstat(file).then(stats=>{if(stats.isFile()){returnfs.readFile(file);}}).then(chunk=>{//TODO}).catch(err=>{//TODO})}总结Promise是好的,它以回调的形式解决了回调地狱和难以管理的错误处理问题,Promise提供了一种链式和线性的方式(.then().then().then()...)来管理我们的异步代码,这种方式是好的,它解决了我们的一些问题,但它并不完美。你会在Async/Await章节看到更好的异步编程问题解决方案,但Promise是基础,请掌握。按照下面的二维码。转载本文请联系七思妙想公众号。