js 异步处理

16. 六月 2017 JavaScript 0

0长10时间没有好好的总结点知识了,最近过的真的太颓废了,看来是要好好的约束一下自己了。

由于前段时间在写公司的机器人程序(没办法,产品用户少,只能是做机器人大军来稳定一下局面不至于太难堪)用的基本上都是异步,各种异步请求等等。采用promise+async解决,现在来总结一下异步的方法。

异步是所有编程语言中都存在的一种流程处理方式,所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

解决异步的方法大概分为以下几种:

  • 回调函数
  • 事件监听
  • 订阅、发布模式
  • promise
  • generator
  • async

回调函数这个很简单,订阅发布模式在之前有篇文章中记录过,promise的使用也在之前的一片文章中简单介绍过。

这篇就介绍一下generator 和 async 。

async 和 generator 算是同源,async是generator的语法糖,用法基本相似。但是我还是偏爱async的,写法比较易懂。

还记得第一遍看es6的文档的时候看到generator就懵逼了,看不懂了。尤其是co 什么的,但是这次看的时候就明了很多了。

generator

generator函数可以看做一个状态机,内部有多个状态,执行 generator 函数会返回一个遍历器对象{value:xxx,done:xxx}  

generato函数是普通函数,但是有两个特征:

  • function  和  方法名之间有个 *  用来标示这是一个generator函数
  • 在方法的内部使用yield来定义不同的状态。

该函数有三个状态:hello,world 和 return 语句(结束执行)。

虽然最后一行调用了此方法,但是不会去执行,这是generator特殊的地方,想要执行必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。而且每次返回的也不是函数的值而是遍历器对象{value:xxx,done:true/false} 。 所以说generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

value属性就是当前yield表达式的值xxxdone属性的值false/true,表示遍历是否结束。

总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

yield 表达式

generator函数可以看做是一种可以暂定的函数,yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

  • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
  • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
  • 如果该函数没有return语句,则返回的对象的value属性值为undefined

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。

for…of 循环

for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法。

上面代码使用for...of循环,依次显示5个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

 

generator 异步Demo

上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。

首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

 

co 模块

解决generator自动执行的方法有很多,最著名的 就是CO模块了。 使用co 的话不需要我们自己去写generator函数的执行器,只要generator 函数传入co中,就会自动执行。

co函数返回一个Promise对象,因此可以用then方法添加回调函数。

处理并发的异步操作

co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。

这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。

下面是另一个例子。

上面的代码允许并发三个somethingAsync异步操作,等到它们全部完成,才会进行下一步。

 

async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

 

上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义。

asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

(4)返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log。

await 命令

正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。

上面代码中,await命令的参数是数值123,它被转成 Promise 对象,并立即resolve

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

 

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

 

如果有多个await命令,可以统一放在try...catch结构中。

使用注意点

第一点,前面已经说过,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

上面两种写法,getFoogetBar都是同时触发,这样就会缩短程序的执行时间。

第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。

第四点,采用for循环不要采用forEach 循环,

如果确实希望多个请求并发执行,可以使用Promise.all方法。

 

项目中的应用可以参见机器人批量注册和获取token的代码。

 

总结、摘自 http://es6.ruanyifeng.com/

 

 


发表评论

电子邮件地址不会被公开。