JavaScript异步编程终极解决方案
JavaScript异步终极解决方案
同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。
相对地,异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。
早期的异步编程
在早期JavaScript中,对异步的处理方式并不理想,主要是采用回调函数的方式。
request('https://example.com',function(err){
if(err){ return ;}
})回调函数
回调函数字面意思就是"回过头调用的函数",回调函数一般事先定义好,然后在某些时候"回过头"来调用,例如我们使用的addEventListener,第二个参数就是一个回调函数,特定事件触发后就会调用这个函数。
let p = document.querSelector('#p');
p.addEventListener('click',funtion (){
// 这是一个回调函数,当事件被触发时执行
})回调函数的概念可以这样理解,执行一个异步操作(这个异步操作可能是一个点击事件,也可能是一个http请求),并且要在异步操作执行完毕后执行一些处理程序,但是由于我们不知道异步操作什么时候执行完,所以我们会先定义好一个函数但是不执行,等异步操作执行完后再调用。
回调函数在Node.js里用的非常多,Node.js大多数异步API都是基于回调函数的。
回调地狱
用回调函数处理异步最大的问题就是容易造成回调地狱。
回调地狱是指多个回调函数深度嵌套,造成代码难以阅读和维护。
想象一下,如果我们需要从后端获取a接口、b接口、c接口中的数据,然后进行某些处理,那么我们的代码可能会像这样:
显而易见,代码复杂且不易维护,开发体验非常不好。
解决回调地狱
JavaScript实现异步最大的问题就是容易出现回调地狱。
随着代码越来越复杂,函数嵌套的就越来越多,这是对于代码维护来说是一件非常痛苦的事情,所以我们会想能不能将里面的代码提取到外面来,比如这样
可惜这种行为很显然会有问题,因为fs.stat是一个异步操作,因此fs.readFile不会等到fs.stat执行完毕后才运行,它很可能在fs.stat还没执行完毕前就执行了,这就导致了逻辑错误。
这里要解决的问题就是当fs.stat执行完毕得出结果时,再执行fs.reaFile,这里很自然的可以想到用一个flag变量来标识fs.stat的执行状态,未执行完为false,执行完为true
这种情况固然能解决我们的问题,但是setInterval这种方法太浪费资源了,不可能大规模使用。我们可以换个思路,可以用发布订阅模式来替代setInterval。
这样能达到我们的要求,我们可以改一下代码,让它更方便使用。
改完之后使用起来方便了很多,我们来分析一下。
对于后执行的函数(即打印后执行的那个函数),我们将他们存放到一个数组fns里,方便遍历执行,而为了能够添加后执行函数,我们添加一个实例方法then,它会将参数(即要添加的后执行的函数)添加到数组fns中。
为了判断先执行的那个函数(即包裹fs.stat的那个函数)什么时候执行完成,我们创建了一个函数resolve,它会遍历执行fns中所有的函数,即所有的后执行的函数,当先执行的函数执行完后,我们调用resolve函数就行了,但是如何将resolve函数传递到先执行的函数中呢?很简单,我们将先执行的函数作为构造函数的参数传入,然后在执行这个函数时把resolve作为参数传入,而在先执行的函数里,我们可以灵活的放置resolve函数的位置。
事实上上面的方法只实现了一些简单的功能,还有不少地方没考虑,比如先执行的函数异常怎么办?上面的方法不能实现函数串联执行,即函数a执行完后执行函数b,函数b执行完后执行函数c。而Promise能解决这些的问题。Promise与上面的方法原理类似,但是功能更强大,逻辑更严谨,Promise是实现JavaScript异步编程的重要工具。
Promise
ECMAScript 6新增的引用类型Promise来处理异步,Promise是一个构造函数,可以通过new操作符来实例化。创建Promise实例时需要传入执行器函数作为参数。
Promise有三种状态:pending、fulfilled(也称resolve)、reject,三者分别表示三种不同的状态:待定(pending)、兑现(fulfilled)、拒绝(reject)。
待定(pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。
Promise有以下两个特点:
对象的状态不受外界影响。
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
这两个特点很好理解,Promise三个状态分别表示异步操作是正在进行中、已经完成、已经失败,如果是正在进行中那么应该让它继续执行,如果是已完成则调用执行成功的处理程序resolve,如果是已失败则执行失败的处理程序reject。而这种状态的改变是不可逆的,一个异步操作执行成功就是成功,不可能一会成功一会又失败,这样很容易导致混乱。
Promise接收一个执行器函数,这个函数有两个参数resolve和reject,这两个参数都是函数,可以被调用和传递参数,一旦调用resolve()就表示Promise对象由pending转变为resolve,会触发Promise.then第一个函数,而一旦调用reject()就表示Promise对象由pending转变为reject,会触发Promise.then第二个函数或Promise.catch函数(两者等价)。而转变后是不可再变的。
Promise也使用到了回调函数,例如Promise.then就接收回调函数作为参数,但是Promise却会不产生回调地狱,因为Promise是链式调用,这种方式让代码便于理解和维护,不会出现回调地狱。
Promise的链式调用不仅能够使代码更易维护,还有其他的优点。比如说在回调函数里需要对每个 回调函数判断是否出现错误,而在Promise中如果出现错误,会顺着调用链一直传递,直到遇到catch()才会被捕获。另外需要注意try...catch无法捕捉Promise中的错误。
Promise能够实现链式调用的秘诀在于,Promise.then()和Promise.catch()的返回值仍然是一个Promise对象。
Promise更多语法这里不再赘述,详情可以看阮一峰老师的文章: 点我
除此之外,Promise还提供多种静态方法。例如之前将的回调地狱,在Promise可以用更优雅的方式解决。
除了all方法外,Promise还有另外五种方法: race、resolve、reject、allSettled、any。
异步函数
ES2017 标准引入了 async函数,使得异步操作变得更加方便。
实现一个异步函数非常简单,只需要在函数声明前加上关键字async就可以了
单是这样的话,异步函数与普通函数没什么不同,异步函数里最重要的还是await关键字,它是实现异步函数的关键,而await关键字必须是在async函数中。
await关键字后面跟着的只能是Promise对象或者是原始值,当异步函数执行的时候,一旦遇到await就会先跳出函数,执行后面的语句,等到异步操作完成,再接着执行函数体内后面的语句。
当执行到await pro;语句时,会跳出异步函数,执行后面的语言打印2,等异步操作执行完成后在执行await后面的语句,打印1.
await关键字会返回Promise对象resolve的第一个参数。
async函数中使用try...catch捕获异常,这点与Promise不同。需要注意的是,如果await后面的Promise对象进入的是reject状态,则异步函数执行会被中断。解决的方法是将await语句没有被包裹在try..catch中或者在Promise对象后添加catch捕获异常。
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
使用async函数需要注意的是,如果使用await,那么await语句所在的函数必须是async函数。也就是说嵌套函数中,最外层是async函数,内层函数是普通函数,那么就不能在内层函数中使用await关键字。
async函数本质上是 Generator函数的语法糖。async函数将Generator函数的星号和yield改成了语义更明确的async和await,另外async函数内置执行器,可以自动执行,还有就是async函数的返回值是Promise对象。
异步函数处理异步操作的方式比Promise更优雅,例如在之前的Promise的链式调用,在异步函数中可以这样写。
最后更新于