当然,我只是在扯淡

js 异步编程,async 函数与 Promise、Generator 函数的比较

 

1Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

 

特点:

1)对象的状态不受外界影响。

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。

缺点:

1)无法取消Promise,一旦新建它就会立即执行,无法中途取消。

2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。

3)当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

 

Promise 新建后就会立即执行。

then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行。

 

对于Promise的嵌套,例如p1中resolve或者reject p2,那么p1的状态取决于p2.

 

Promise.prototype.then

作用是为 Promise 实例添加状态改变时的回调函数。

第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。

因此可以采用链式写法,即then方法后面再调用另一个then方法。

采用链式的then,可以指定一组按照次序调用的回调函数。

执行时间:会在本轮“事件循环”(event loop)的结束时执行。

 

Promise.prototype.catch

  catch是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

  如果 Promise 状态已经变成resolved,再抛出错误是无效的。

  Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

 

Promise.prototype.finally

用于指定不管 Promise 对象最后状态如何,都会执行的操作。

不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

 

Promise.all

用于将多个 Promise 实例,包装成一个新的 Promise 实例。

如果不是 Promise 实例,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

两种情况:

1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。

 

Promise.race

同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

如果不是 Promise 实例,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

只要多个实例之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。(请求超时)

 

Promise.resolve

将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。

四种情况:

1)参数是一个 Promise 实例

将不做任何修改、原封不动地返回这个实例。

  (2)参数是一个thenable对象

thenable对象指的是具有then方法的对象,会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

  (3)参数不是具有then方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。

Promise.resolve方法的参数,会同时传给回调函数。

  (4)不带有任何参数

允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法。

需要注意的是,立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

 

Promise.reject

也会返回一个新的 Promise 实例,该实例的状态为rejected

注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

 

Promise.try

实际开发中,经常遇到一种情况:

不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。

因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。

一般就会采用下面的写法:

  

Promise.resolve().then(f)

上面的写法有一个缺点,就是如果f是同步函数,那么它会在本轮事件循环的末尾执行。

鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法替代上面的写法。

 

 

 

2generator/yield

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

多种理解角度:

语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。

1)function关键字与函数名之间有一个星号;

2)函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。

不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

 

注意

yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。(定义的时候就会报错,不会等到next时)

yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

yield与return

相似之处在于,都能返回紧跟在语句后面的那个表达式的值。

区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

 

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

 var myIterable = {};

   myIterable[Symbol.iterator] = function* () { yield 1;  yield 2;  yield 3; };

   [...myIterable] // [1, 2, 3]

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

function* gen(){ … }

 var g = gen();

 g[Symbol.iterator]() === g

next 方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。

next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。(第一个next方法的参数是无效的)

 

for...of 循环 

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

注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象。(return语句,不包括在for...of循环之中)

利用for...of循环,可以写出遍历任意对象(object)的方法。

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

 

for...of循环、扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。

 

Generator.prototype.throw

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获(只能捕获一次)。

注意:遍历器对象的throw与全局对象的throw方法是不一样的。

  如果Generator函数中没有定义catch语句,就会被外部的catch捕获。

 

Generator.prototype.return

返回给定的值,并且终结遍历 Generator 函数。

遍历器对象调用return方法后,返回值的value属性就是return方法的参数,返回值的done属性为true。

并且,Generator 函数的遍历就终止了,以后再调用next方法,done属性总是返回true。

注意:如果 Generator 函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完再执行。(即最后才执行return方法)

 

next()、throw()、return()的共同点

next()是将yield表达式替换成一个值。

throw()是将yield表达式替换成一个throw语句

return()是将yield表达式替换成一个return语句。

 

yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。

这个就需要用到yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。

从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

反之,在有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。

如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。

实际上,任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

 

yield*命令可以很方便地取出嵌套数组的所有成员。

 

作为对象属性的 Generator 函数

  let obj = {

    * myGeneratorMethod() {  ··· }

  };

 

Generator 函数的this

Generator 函数在this对象上面添加了一个属性a,但是生成的遍历器对象拿不到这个属性。

new命令跟Generator 函数一起使用,结果报错,因为Generator 函数不是构造函数。

那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?

使用call或者apply方法,动态的绑定Generator 函数内的this对象,使用Generator 函数的原型对象即可。

应用

1)异步操作的同步化表达

使用同步的方式书写函数,在异步的回调中调用next方法

2)控制流管理 

如果有一个多步操作非常耗时,采用回调函数,会出现多层嵌套。

使用Promise会进一步改善代码流程,使用Generator 函数还可以进一步改善代码运行流程。然后,使用一个函数,按次序自动执行所有步骤。

3)部署 Iterator 接口

利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

4)作为数据结构

Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,

因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。

 

Generator 函数的异步应用

传统方法

回调函数

事件监听

发布/订阅

Promise 对象

Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。

基本概念

Generator 函数的数据交换和错误处理

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。

除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

 

Thunk 函数

Thunk 函数是自动执行 Generator 函数的一种方法。

含义

编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

 

JavaScript 语言的 Thunk 函数 

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

 

 

3async/await

最关心的问题怎么将异步变成同步?

(同步就使用 await ,而 await 后面接一个 Promise 即可,会同步的等待并且拿到 Promise的结果)

含义

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

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

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

 

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

1)内置执行器

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。

也就是说,async函数的执行,与普通函数一模一样,只要一行。

2)更好的语义

async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

3)更广的适用性

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,

async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

(4)返回值是 Promise has of

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。

 

基本用法

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

 

语法 

async函数的语法规则总体上比较简单,难点是错误处理机制。

返回 Promise 对象

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

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

 

Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。

也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

 

await 命令

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

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

只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行。(如果不希望中断执行可以try...catch)

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

 

错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

防止出错的方法,也是将其放在try...catch代码块之中。

或者为await后面的Promise对象添加catch方法。

 

使用注意点

1、await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

2、多个await命令后面的异步操作,如果不存在继发关系(比较耗时),最好让它们同时触发。

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

async 函数的实现原理

是将 Generator 函数和自动执行器,包装在一个函数里。

 

 async 函数与 PromiseGenerator 函数的比较

Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(then、catch等等),操作本身的语义反而不容易看出来。

Generator 函数语义比 Promise 写法更清晰,这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数。

Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。

它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。

如果使用 Generator 写法,自动执行器需要用户自己提供。

 

 

特别说明:本文中的很多内容都是参考阮一峰老师的ECMAScript 6 入门,如有转载请注明出处。

 

posted @ 2018-04-12 15:32  胜天尊  阅读(2410)  评论(0编辑  收藏  举报