js中的异步流程控制--Promise/Generator/Async/Await
长文预警 ~
异步I/O、事件驱动使JS这个单线程语言在不阻塞的情况下可以并行的执行很多任务,这带来了性能的极大提升,并且更加符合人们的自然认识(烧一壶水,期间你肯定不会等着水烧开再去做别的事,异步才是正常的啊!)。然而异步风格也给流程控制,错误处理带来了更多的麻烦。
一、回调
回调是JS的基础,函数可以作为参数传递并在恰当的时机执行,比如有下面的三个函数:
如果 f1
中存在异步操作,比如 ajax
请求,并且 f2
需要在 f1
执行完毕之后执行,那么可以使用回调的方式改写函数,如下:
使用这种方式, f1
的异步操作,不会阻碍程序的运行,并且可以很方便的控制函数的执行过程,显然,我要说但是了。如果你看到下面的代码,估计你不会觉得回调有那么美好了。
WTF?!
可以看出,回调的缺点很明显,各个函数高度耦合,代码结构混乱,debug
困难,等等。
二、事件监听(观察者模式)
另一种解决异步流程控制的方法是采用事件监听的机制,某个事件的触发不再以某个时机为界限,而是取决于某个事件是否触发。
唔,很美好的解决方案,但是观察者模式的缺点在其中也体现的很明显,事件的监听和触发散落在不同的地方,程序趋于复杂之后,Event
机制的复杂度也极大提高,明显这不是我们追求的。
三、异步流程控制库
为了优雅的解决异步流程控制的问题,伟大的猿们前赴后继,产出了很多方案,造就了不少优秀的库,包括但不限于 q
co
async
等。
这些库的具体实现或使用方式不在本文的谈论范围,暂时跳过。
四、新标准、新未来
重点来了!
现在已经是2016年了,ES
的标准一代快过一代,有了 bable
这样的工具,甚至 ES7
都不再是不可触及的 feture
了,新的标准当然对异步控制做出了很多努力,让我们一个一个来看。
1、Promise
所谓的 Promise
,就是一个特殊的用于传递异步信息的对象,它代表一个未完成的但是将会完成的操作。也就是说,Promise
代表了某个未来才会知道结果的事件(通常是一个异步操作),并且为这个异步事件提供统一的 API
,能够让使用者准确的控制异步操作的每一个流程。
a. 基本理解
一个
Promise
对象,存在三种状态,pending(进行中)
、resolve(已完成)
、reject(已失败)
。一个异步操作的开始,对应着Promise
的pending
状态,异步操作的结束,对应着另两种状态,当异步操作成功时,对应着resolve
状态,失败时对应着reject
状态。Promise
的状态如果发生改变,就不能再被更改,并且,只能由pending
向另外两种状态转变,不能逆,也不能resolve
和reject
互相转化。
b. 基本 API
Promise.resolve()
Promise.reject()
Promise.prototype.then()
Promise.prototype.catch()
Promise.all()
Promise.race()
c. 详解
创建
Promise
实例构造函数
Promise
接受一个函数作为参数,这个函数又有两个类型为方法的参数,resolve
、reject
。resolve
方法用来将promise
从pending
状态转换到resolve
状态,并且将异步操作成功后返回的内容传递出去,reject
方法用来将promise
从pending
状态转换到reject
状态,在异步操作失败时调用,并传递错误信息。调用
Promise
实例创建后,可以调用then
方法,处理异步操作成功或失败的状态。then
方法接受两个函数参数,第一个即为创建Promise
实例时的resolve
函数,第二个则为创建Promise
实例时的reject
函数,用来分别处理异步操作成功,或失败的后续操作。当然,第二个用来处理失败的参数为可选参数。示例1:
sleep
函数在很多编程语言中,都有着
sleep
函数,延迟程序执行,javascript
中可以用setTimeout
完成操作的延迟执行,但是还是需要使用回调的方式,现在让我们用Promise
来实现。一个简单的休眠函数就完成了,调用更加方便,也更加直观。
示例2: 异步
Ajax
请求Promise.prototype.then()
上面两个简单的示例,展示了
Promise
的基本使用方法,让我们再来看看具体的API
。then
方法除了用于处理Promise
实例的成功或失败操作,还会返回一个新的Promise
实例,并且将返回值传递给下一层then
方法,即:这样来看,曾经使用多层嵌套的回调来控制异步流程的代码终于可以下岗了。
Promise.prototype.catch()
在
then
方法中,第二个参数可以对当前Promise
中的错误进行处理,为了统一的错误处理,Promise
也为我们提供了一个更加方便的错误处理方式。当一个
Promise
实例转变为reject
状态的时候,会调用catch
中的回调函数,并且把首次reject
的错误传递进去。catch
能够捕获reject
主动抛出的错误,同样也能捕获Promise
运行中的错误。catch
捕获错误时具有冒泡属性,即在最后调用catch
时,能够捕获到此前所有Promise
中的错误。上面的示例中,最后的
catch
方法能够捕获到前两个Promise
中任意一个产生的错误。Promise.all()
Promise.all
方法用于将多个Promise实例,包装成一个新的Promise实例。Promise.all
接受一个由多个Promise
实例组成的数组,如果数组中存在非Promise
的示例,则allPromise
的状态直接为reject
。allPromise
的状态由p1/p2/p3
共同决定,三个全部resolve
则allPromise
转变为resolve
,其中任意一个出现reject
,则allPromise
转变为reject
。Promise.race()
Promise.race
方法同样用于将多个Promise实例,包装成一个新的Promise实例。与
Promise.all
不同的是,如果p1/p2/p3
中有任意一个状态先发生了变化,则allPromise
的状态也会跟着转变,并且状态与最先发生状态改变的promise
一致。
d. 实际应用
图片加载
Promise
风格的文件读写
2、Generator
想象这样的一个场景:
当你执行一个函数的时候,需要在某个时间点停下来等待另一个操作完成,并且拿到这个操作的执行结果,然后继续执行。
这样的场景就是 ES6
的生成器需要解决的问题。
a. 基本理解
生成器本质上是一种特殊的迭代器,迭代最简单的例子如下:
而生成器作为一种特殊的迭代器就是它的每一次迭代都是可控的,详情下面将具体描述。
生成器形式上是一种函数,只不过比普通的函数
function
多一个*
,即function*(){}
。
b. 基本API
function*(){}
yield
Generator.prototype.next()
Generator.prototype.return()
Generator.prototype.throw()
yield*
c. 详解
Generator
函数上面的例子就是一个简单的
Generator
函数,可以发现,函数声明是多个一个*
,并且函数体内出现了多个yield
语句和return
语句,即该生成器函数存在四种迭代状态:hello
world
!
return
但是当我们执行上述代码的时候,发现并没有即时的执行,返回的也不是它的执行结果,而是一个生成器对象,只有当调用这个生成器对象的
next
方法,才会依次的执行函数语句,直到遇到yield
语句或return
语句。让我们梳理一下上述代码的执行流程。
第一次调用
next
: 生成器函数开始执行,遇到yield
语句,暂停执行。next
返回一个对象,其中将当前yeild
语句的值hello
作为返回对象的value
字段。done
字段为false
,迭代未结束。第二次调用
next
: 从上一个yield
语句开始执行,遇到yield
语句,暂停执行。next
返回一个对象,其中将当前yeild
语句的值world
作为返回对象的value
字段。done
字段为false
,迭代未结束。第三次调用
next
: 从上一个yield
语句开始执行,遇到yield
语句,暂停执行。next
返回一个对象,其中将当前yeild
语句的值!
作为返回对象的value
字段。done
字段为false
,迭代未结束。第四次调用
next
: 从上一个yield
语句开始执行,遇到return
语句,结束执行。next
返回一个对象,其中将当前return
语句的值func end
作为返回对象的value
字段。done
字段为true
,迭代结束。第五次调用
next
: 生成器函数已经迭代(运行)完毕,next
方法始终返回{value: undefined, done: true}
让我们再用一个例子来了解一下
yield
语句的执行流程:首次调用
next
,函数开始执行,遇到yield
暂停执行,将yield
语句后的表达式运行后返回,当作next
方法返回值的value
字段,依次调用next
,从上次yield
处继续运行,直到遇到下一个yield
,循环往复。yield
语句通过上面的例子,
yield
语句的特性已经很明显:yield
语句会暂停生成器函数的执行yield
语句后表达式的运行结果将作为next
语句返回值中的value
字段
Generator.prototype.next()
next
语句的返回值有两个字段value
和done
,value
为当前next
指向的yield
语句的返回值,done
标识当前生成器函数是否迭代完毕。next
方法还可以接受任意一个参数,该参数将作为上一个yield
返回值。上面的代码实现了一个无限的迭代器,在每次运行到
yield
语句时,如果调用指向此次yield
语句的next
方法没有参数,那么reset
的值始终是undefined
。只有在调用next
方法传入了参数,此次执行yield
语句时,yield
语句的返回值将变为next
传入的参数。这样的特性能够让我们用同步的方式写出异步执行的代码,具体例子下文。Generator.prototype.return()
当我们想在外部结束生成器函数的迭代,可以使用
return
方法,并将return
方法的参数作为返回值。Generator.prototype.return()
throw
方法允许我们在生成器函数外部抛出错误,并在内部捕获。第一次抛出错误,被生成器函数捕获到,第二次再抛出,由于
catch
语句已经在第一次执行过了,所以内部无法再次捕获错误,从而在外部的try catch
语句中可以捕获到错误。yield*
如果想在生成器函数中调用另一个生成器函数,将会用到
yield*
语句。
d. 实际应用
异步
Ajax
请求上面的代码中,第一次调用
next
方法,开始请求,拿到返回结果后,用结果中的value
(fetch
返回的是一个Promise
,所以需要then
方法调用),调用下一次then
从而执行生成器函数中yield
后面的代码。可以看出,虽然生成器函数将异步操作表示的很简洁,但是流程管理并不是很直接,即何时执行第一阶段,何时执行第二阶段并不能很好的向使用者展示。
3、Async/Await
从回调,到 Promise
,再到 Generator
函数,js的异步流程控制一直在进化,但是每种解决方法都无形的增加了额外的复杂度,都需要理解底层的运行机制才能很好的运用。
而 ES7
提出的 Async/Await
,大概也许可能是 JavaScript 中最好的异步解决方案。
a. 实例
异步读取文件
如果把上面的代码写成
Geneerator
风格,你会发现两者很相似。对比之后,其实
async
函数就是把*
替换成async
,把yield
替换成await
。可以说,
async
其实就是对Geneerator
的语法糖,只不过多包了一层,改进了很多。第一,使用
async
函数不用再手动的调用next
方法来执行每一次迭代第二,更好的语义,
async
表示这个函数是一个异步函数,await
表示此后的操作需要等待此步操作完成第三,侵入性更低,原生的
try catch
语句能处理错误,async
函数中的await
语句不用做特殊处理,Promise
可以,原始的同步操作也可以第四,更直观、更灵活的调用,
async
函数返回的是一个Promise
对象,异步操作完成后可以直接用then
方法进行下一步操作第五,简单的API,只有
async
和await
两个API,async
用来声明一个异步函数,await
用来等待一个异步操作sleep
函数上文我们用
Promise
实现了一个异步风格的sleep
函数,现在让我们看看如何用同步的风格实现并使用它。完美~
b. 如何使用
async
await
特性属于ES7的新特性,目前的ES运行环境中并没有实现这样的功能,但是借助 babel
,我们可以很方便的使用这些新特性。
这个展开讲又是一个大话题~贴一个 bable
转换代码的网址:Babel transform online
如何在线下使用,自行谷歌,或者,再来一篇?哈哈
五、结束
长长的文章终于结束了,呼~
主要的目的就是对异步流程的解决方案进行一下梳理,加深对js异步特性的理解。最推荐的方式还是ES7的新特性,毕竟是既有的新标准,使用的过程还能学习下 babel
的配置,哈哈。