解决回调地狱
文中将简单整理一下js异步编程的方法。关于js异步编程,早期很容易出现回调地狱这一现象,也出现了一些相应的解决办法,从Promise
到Generator
再到async/await
,解决了回调地狱这一问题,当然本文只是浅显的举出一些实例,以及个人简单的理解。
异步编程这一块确实还有很多可以继续深入的地方。
异步编程方法
- 回调函数
- 事件监听
- Promise对象
- Generator函数
- Generator的语法糖async/await
文中将着重放在Promise
,Generator函数
,async/await
这三个方面进行举例
回调地狱产生
这里首先使用node随便写三个接口测试,具体代码就不贴了,两个Get请求,一个Post请求。
1 | $.get(url + "/first", function(resFirst) { |
可以看到上面代码就是一个简单的回调地狱,每一个请求都是外层请求的回调。那么有什么问题呢?最明显的一点就是,代码可读性差,就像洋葱一样一层包裹着一层,改动一处,其余地方也要改动,可维护性也不好。
当然回调地狱不止是这么一个问题,还有一个问题就是异常处理上的问题,即在回调中出现的异常无法被捕获,举个例子
1 | function throwError(){ |
上面代码运行后并不会弹出窗口,也就是无法被捕获到,那么类比到最开始举的三个请求的例子,我们不能直接在三个请求整体的外面写try/catch,因为这样无法捕获,而需要在每个回调内部写才能捕获到,如下
1 | $.get(url + "/first", function(resFirst) { |
这样代码的弊端非常明显,代码量不仅大,而且异常非常不方便处理,那么下面就将使用Promise改写该段代码,解决上述问题。
Promise改写
Promise是一种异步编程解决方案,有三种状态pending
,fulfilled
,rejected
,使用Promise就可以让上面的代码异步操作以同步操作的流程写出来,避免过多嵌套,每一个then
都可以当成回调,根据链式调用,其参数是上一个取决于上一个链发送的参数
1 | // 封装的请求 |
上面代码的上部分是使用Promise实现Ajax操作,下部分则是三个请求,虽然还是有点丑,可以进一步封装,但是确实把嵌套打开了。并且最重要的是异常捕获没有问题,resolve
代表可以继续往下进行,reject
则表示出错,只需要在最后面写上catch
既可以全部捕获
Promise实现原理
这里照着链接敲了一遍,然后自己打断点跑了几遍,也算是理解了Promise的大致实现流程。
首先分析一下,Promise一共三种状态,pending
,resolved
,rejected
三种状态,然后常用方法两种then
和catch
。那么我们要做的就是将这些逐个实现。
首先就是构造函数的编写,这里就不贴代码了,想看代码得可以去上面链接自己敲一遍。由上述可得,内部属性status
用来存三种状态,data
用来存resolve
传入的数据。然后Promise
接收的参数是一个函数,函数有两个参数,分别是resolve
和reject
两个方法。因此,构造函数还需要实现resolve
和reject
两个方法,方法功能是调用后将状态改变,以及将resolve
或reject
的参数存入data
。这里有一个要注意的地方就是,Promise
的状态一经改变,就会凝固,不会再改变了。所以这里要注意一下。
然后就是then
方法的实现,then
方法接收两个参数,一个是成功回调函数,一个是失败回掉函数。而且这里的then
返回的是新的Promise实例,但是属性还是之前的数据,原因是,假如是同一个对象,那么假如then
的promise
抛出一个异常的话,状态就变成了rejected
,这就违背了Promise
状态一经改变就不会再变的原则。之后then
内部会调用传入的回掉函数,并改变此时的promise
对象属性。
需要注意的一点是,new Promise(resolve => resolve(8)).then().then((value) => console.log(value)})
这种情况下需要值穿透,方法就是假如then
不传参数的话,我们默认给它一个参数,让其return自己,就能实现值穿透。
catch
方法的话实现起来就比较简单了,直接调用then(null, onRejected)
即可。
这里有一点要注意的,就是之前构造函数里还有两个属性_self.onResolvedCallbacks
,_self.onRejectedCallbacks
这两个属性分别是数组,存的是函数。之前一直不了解两个属性的作用。后来仔细查看之后,这两个属性存的是状态pending
情况下的回调。那么是什么意思呢,意思就是说当触发then
方法的时候,status
有可能是pending
状态,那么这个时候并不知道是要调用成功回调还是失败回调。拿方案就是都存下来,当后面状态改变的时候,当触发resolve
时,会遍历callback
数组,并执行函数。我们可以用一段代码,打断点测试一下,如下
1 | const promise = new MPromise(function(resolve, reject) { |
上述代码的第一个then时,状态还是pending,就会触发存入callback的操作。而且后面有一个无参数的then,可以测试值穿透的情况。
那么实现原理大概就是这样了,自己动手写一遍就很清楚了。其实我上面讲的肯定很乱,没有代码空口白说,直接看估计也看不懂。主要是为了自己梳理一下思路吧,想具体了解原理的请点上述链接自行查看。
Generator改写
Generator封装了多个内部状态,其使用next()
来继续运行,使用throw()
来抛出错误。内部使用yield
来定义内部状态
1 | function* reqFun() { |
代码如上图,发送请求依然是用上面Promise封装的ajax操作,优点代码更加简洁,而且也解决了在外部使用一个try/catch
就能捕获内部所有状态
其中需要手动执行函数,并使用next()
让其继续运行。当然可以使用co模块让其自动运行,这里就不再赘述
async函数改写
async函数使异步操作更加方便,简单讲就是Generator函数的语法糖,其内置了执行器,会自动执行结果
1 | async function reqFun() { |
和上面Generator改写的很相似,async有更广的适应性,其返回值是Promise,也就意味着定义的reqFun()
函数可以继续then()
或者catch()
,可以构造更加合理的代码结构
总结
文中只是整理了一下基本的使用,以及自己的一些见解吧,也参考了很多博客。也算是对ES6的一些内容的进一步实践吧
- 感谢你的欣赏!