JavaScript之异步

来源:互联网 发布:java将图片生成二维码 编辑:程序博客网 时间:2024/06/09 16:52

JavaScript之异步 - Promise (二)

1. 链式流


(1) 介绍

Promise的单步执行this-then-that并不是唯一的机制,我们可以将多个promise连接一起表示一系列异步步骤。

这种方式可以实行的关键在于两个Promise固有行为特性。
        每次你对Promise调用then(),都会创建一个新的Promise,我们可以将其连接起来
       不管从then()调用完成的回调(第一个参数,因为返回值只有一个,前面文章提到过)返回的值是什么,都会被自动设置为连接Promise的完成(上一个promise的返回值传到下一个promise中)。

先看一段代码
var p = Promise.resolve( 21 );var p2 = p.then( function(v){console.log( v ); // 21// 用值42填充p2return v * 2;} );// 连接p2p2.then( function(v){console.log( v ); // 42} );
我们通过返回 v * 2( 即 42),完成了第一个调用 then(..) 创建并返回的 promise p2。 p2 的then(..) 调用在运行时会从 return v * 2 语句接受完成值。当然, p2.then(..) 又创建了另一个新的 promise,可以用变量 p3 存储。

但是,如果必须创建一个临时变量 p2(或 p3 等),还是有一点麻烦的。谢天谢地,我们很容易把这些链接到一起:
var p = Promise.resolve( 21 );p.then( function(v){console.log( v ); // 21// 用值42完成连接的promisereturn v * 2;} ).then( function(v){console.log( v ); // 42} );
现在第一个 then(..) 就是异步序列中的第一步,第二个 then(..) 就是第二步。这可以一直任意扩展下去。只要保持把先前的 then(..) 连到自动创建的每一个 Promise 即可。

我们再看一下向封装的 promise 引入异步
var p = Promise.resolve( 21 );p.then( function(v){console.log( v ); // 创建一个promise并返回return new Promise( function(resolve,reject){// 引入异步!setTimeout( function(){// 用值42填充resolve( v * 2 );}, 100 );} );} ).then( function(v){// 在前一步中的100ms延迟之后运行console.log( v ); // 42} );
在这些例子中,一步步传递的值是可选的。如果不显式返回一个值,就会隐式返回undefined,并且这些 promise 仍然会以同样的方式链接在一起。这样,每个 Promise 的决议就成了继续下一个步骤的信号。

请看下面图片中没有返回值时,下一个then接受的参数为undefined.

(2) 没有错误处理函数

如果这个 Promise 链中的某个步骤出错了怎么办?错误和异常是基于每个 Promise 的, 这意味着可能在链的任意位置捕捉到这样的错误,而这个捕捉动作在某种程度上就相当于在这一位置将整条链“重置”回了正常运作:
function delay(time) {return new Promise( function(resolve,reject){setTimeout( resolve, time );} );}delay( 100 ) // 步骤1.then( function STEP2(){console.log( "step 2 (after 100ms)" );testErrorFunction(); //undefinedreturn delay( 200 );} ).then( function STEP3(){console.log( "step 3 (after another 200ms)" );} ,function rejected(err){console.log( err );}).then( function STEP4(){console.log( "step 4 (next Job)" );return delay( 50 );} ).then( function STEP5(){console.log( "step 5 (after another 50ms)" );} )
这段代码在step2中出现了错误,但是由于我们在step3中添加了错误处理函数,所以会在step3中拒绝处理函数会捕捉到这个错误,调用第二个参数有返回值的话会传给下一个步骤,然后继续step4。


但是如果我们的错误处理函数不是在step3 中,而是在step5中,那么会发生什么呢?我们看一下图示:


如你所见,默认拒绝处理函数只是把错误重新抛出,这最终会使得 p2(链接的 promise)
用同样的错误理由拒绝。从本质上说,这使得错误可以继续沿着 Promise 链传播下去,直到遇到显式定义的拒绝处理函数,也就是我们看到的step5中的处理函数,大家想一下,如果没有错误处理函数的话,就会直接在浏览器控制台报错,由此可见,错误处理函数的重要性。默认拒绝处理函数也可能默默丢失掉,浏览器都检测不到,这个的话认识到就行了。


(3) 没有完成处理函数

如果没有给 then(..) 传递一个适当有效的函数作为完成处理函数参数,还是会有作为替代的一个默认处理函数:
var p = Promise.resolve( 42 );p.then(// 假设的完成处理函数,如果省略或者传入任何非函数值// function(v) {// return v;// }null,function rejected(err){// 永远不会到达这里}).then(function(param) {console.log(param); //42},function rejected(err) {console.log(err)});

你可以看到,默认的完成处理函数只是把接收到的任何传入值传递给下一个步骤
( Promise)而已。

Note: 
then(null,function(err){ .. }) 这个模式——只处理拒绝(如果有的话),但又把完成值传递下去——有一个缩写形式的 API: catch(function(err)
{ .. })。下一小节会详细介绍 catch(..).

(4)总结

让我们来简单总结一下使链式流程控制可行的 Promise 固有特性。
• 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。
• 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise 就相应地决议。
• 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议
值是什么,都会成为当前 then(..) 返回的链接 Promise 的决议值。

尽管链式流程控制是有用的,但是对其最精确的看法是把它看作 Promise 组合到一起的一个附加益处,而不是主要目的。正如前面已经多次深入讨论的, Promise 规范化了异步,并封装了时间相关值的状态,使得我们能够把它们以这种有用的方式链接到一起。

当然,相对于前面讨论的回调的一团乱麻,链接的顺序表达( this-then-this-then-this...)已经是一个巨大的进步。但是,仍然有大量的重复样板代码( then(..) 以及 function(){ ... })。在后面的文章中(有机会的话),我们将会看到在顺序流程控制表达方面提升巨大的优美模式,通过生成器实现。

(5) 术语: 决议、 完成以及拒绝

对于术语决议( resolve)、 完成( fulfill)和拒绝( reject),在更深入学习 Promise 之前,我们还有一些模糊之处需要澄清。先来研究一下构造器 Promise(..):
var p = new Promise( function(X,Y){// X()用于完成// Y()用于拒绝} );
前面的文章中我们使用了x,y两个处理函数,但是没有对其命名进行规范,这里我们的命名为:
var p1 = new Promise( function(resolve,reject){} );p1.then(function fulfilled(){},function rejected(err){});
构造器中的函数命名为resolve,reject,then中处理函数命名为fulfilled,rejected。

2. 错误处理


对多数开发者来说,错误处理最自然的形式就是同步的 try..catch 结构。遗憾的是,它只能是同步的,无法用于异步代码模式:
function foo() {setTimeout( function(){baz.bar();}, 100 );}try {foo();// 后面从 `baz.bar()` 抛出全局错误}catch (err) {console.log(error)}
请看图示,看了前面的文章相信大家可以想到原因,因为时间循环中settimeout已经是下一个事件队列了。

Promise 错误处理就是一个“绝望的陷阱”设计。默认情况下,它假定你想要Promise 状态吞掉所有的错误。如果你忘了查看这个状态,这个错误就会默默地(通常是绝望地)在暗处凋零死掉,前面也有提到过,看下面图示,尽管控制台在打出,但是没有阻止其他代码的运行,用户毕竟不是开发者,日常使用中这个错误是不会起到任何作用中的,当然,用户用户会发现系统中一些功能没有体现出来,毕竟上面的代码中有了错误。

为了避免丢失被忽略和抛弃的 Promise 错误,一些开发者表示, Promise 链的一个最佳实践就是最后总以一个 catch(..) 结束,比如:
这个问题是解决了,但是又会有另外的问题,如果 handleErrors(..) 本身内部也有错误怎么办呢?谁来捕捉它?还有一个没人处理的promise: catch(..) 返回的那一个。我们没有捕获这个 promise 的结果,也没有为其注册拒绝处理函数。
var p = Promise.resolve( 42 );p.then(function fulfilled(msg){// 数字没有string函数,所以会抛出错误console.log( msg.toLowerCase() );}).catch( handleErrors );


你并不能简单地在这个链尾端添加一个新的 catch(..),因为它很可能会失败。任何Promise 链的最后一步,不管是什么,总是存在着在未被查看的 Promise 中出现未捕获错误的可能性,尽管这种可能性越来越低。看起来好像是个无解的问题吧?所以在书写错误处理函数的时候是需要小心的。

3. Promise模式


(1). Promise.all([])

在异步序列中( Promise 链),任意时刻都只能有一个异步任务正在执行——步骤 2 只能在步骤 1 之后,步骤 3 只能在步骤 2 之后。但是,如果想要同时执行两个或更多步骤(也就是“并行执行”),要怎么实现呢?

假定你想要同时发送两个 Ajax 请求,等它们不管以什么顺序全部完成之后,再发送第三
个 Ajax 请求。考虑:
// request(..)是一个Promise-aware Ajax工具// 就像我们在本章前面定义的一样var p1 = request( "http://some.url.1/" );var p2 = request( "http://some.url.2/" );Promise.all( [p1,p2] ).then( function(msgs){        // 这里,p1和p2完成并把它们的消息传入        return request(                "http://some.url.3/?v=" + msgs.join(",")        );}).then( function(msg){console.log( msg );} )
Promise.all([ .. ]) 需要一个参数,是一个数组,通常由 Promise 实例组成。从 Promise.all([ .. ]) 调用返回的 promise 会收到一个完成消息(代码片段中的 msg)。这是一个由所有传入 promise 的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)。



Note: 严格说来, 传 给 Promise.all([ .. ]) 的数组中的值可以是Promise、thenable,甚至是立即值。就本质而言,列表中的每个值都会通过 Promise.resolve(..) 过滤,以确保要等待的是一个真正的 Promise,所以立即值会被规范化为为这个值构建的 Promise。如果数组是空的,主 Promise 就会立即完成。

从 Promise.all([ .. ]) 返回的主 promise 在且仅在所有的成员 promise 都完成后才会完成。如果这些 promise 中有任何一个被拒绝的话,主 Promise.all([ .. ])promise 就会立即被拒绝,并丢弃来自其他所有 promise 的全部结果。

永远要记住为每个 promise 关联一个拒绝 / 错误处理函数,特别是从 Promise.all([ .. ])返回的那一个。

(2) Promise.race([ .. ])

尽管 Promise.all([ .. ]) 协调多个并发 Promise 的运行,并假定所有 Promise 都需要完成,但有时候你会想只响应“第一个跨过终点线的 Promise”,而抛弃其他 Promise。这种模式传统上称为门闩,但在 Promise 中称为竞态。墙面回调的文章里也提到过。

Promise.race([ .. ]) 也接受单个数组参数。这个数组由一个或多个 Promise、 thenable 或立即值组成。立即值之间的竞争在实践中没有太大意义,因为显然列表中的第一个会获胜,就像赛跑中有一个选手是从终点开始比赛一样!

与 Promise.all([ .. ]) 类似,一旦有任何一个 Promise 决议为完成, Promise.race([ .. ])就会完成;一旦有任何一个 Promise 决议为拒绝,它就会拒绝。
请看例子:

一项竞赛需要至少一个“参赛者”。所以,如果你传入了一个空数组,主race([..]) Promise 永远不会决议,而不是立即决议。

(3). 并发迭代

我们考虑一下一个异步的 map(..) 工具。它接收一个数组的值(可以是Promise 或其他任何值),外加要在每个值上运行一个函数(任务)作为参数。 map(..) 本身返回一个 promise,其完成值是一个数组,该数组(保持映射顺序)保存任务执行之后的异步完成值:
Promise.map = function(vals,cb) {// 一个等待所有map的promise的新promisereturn Promise.all(// 注:一般数组map(..)把值数组转换为 promise数组vals.map( function(val){// 用val异步map之后决议的新promise替换valreturn new Promise( function(resolve){cb( val, resolve );} );} ));};var p1 = Promise.resolve( 21 );var p2 = Promise.resolve( 42 );//var p3 = Promise.reject( "Oops" );// 把列表中的值加倍,即使是在Promise中Promise.map( [p1,p2], function(pr,done){// 保证这一条本身是一个PromisePromise.resolve( pr ).then(// 提取值作为vfunction(v){// map完成的v到新值done( v * 2 );});} ).then( function(vals){console.log( vals ); // [42,84,"Oops"]} );


4. Promise API


(1) new Promise(..) 构造器

有显示性的构造器 Promise(..) 必须和 new 一起使用,并且必须提供一个函数回调。这个回调是同步的或立即调用的。这个函数接受两个函数回调,用以支持 promise 的决议。通常我们把这两个函数称为 resolve(..) 和 reject(..):
var p = new Promise( function(resolve,reject){// resolve(..)用于决议/完成这个promise// reject(..)用于拒绝这个promise} );
reject(..) 就是拒绝这个 promise;但 resolve(..) 既可能完成 promise,也可能拒绝,要根据传入参数而定。如果传给 resolve(..) 的是一个非 Promise、非 thenable 的立即值,这个 promise 就会用这个值完成。

但是,如果传给 resolve(..) 的是一个真正的 Promise 或 thenable 值,这个值就会被递归展开,并且(要构造的) promise 将取用其最终决议值或状态。


(2) Promise.resolve(..) 和 Promise.reject(..)

创建一个已被拒绝的 Promise 的快捷方式是使用 Promise.reject(..),所以以下两个
promise 是等价的:

var p1 = new Promise( function(resolve,reject){reject( "Oops" );} );var p2 = Promise.reject( "Oops" );
Promise.resolve(..) 常用于创建一个已完成的 Promise,使用方式与 Promise.reject(..)类似。但是, Promise.resolve(..) 也会展开 thenable 值。在这种情况下,返回的 Promise 采用传入的这个 thenable 的最终决议值,可能是完成,也可能是拒绝:

(3) then(..) 和 catch(..)

每个 Promise 实例(不是 Promise API 命名空间)都有 then(..) 和 catch(..) 方法,通过这两个方法可以为这个 Promise 注册完成和拒绝处理函数。 Promise 决议之后,立即会调用这两个处理函数之一,但不会两个都调用,而且总是异步调用。

then(..) 接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。

就像刚刚讨论过的一样, catch(..) 只接受一个拒绝回调作为参数,并自动替换默认完成回调。换句话说,它等价于 then(null,..):
p.then( fulfilled );p.then( fulfilled, rejected );p.catch( rejected ); // 或者p.then( null, rejected )
then(..) 和 catch(..) 也会创建并返回一个新的 promise,这个 promise 可以用于实现Promise 链式流程控制。如果完成或拒绝回调中抛出异常,返回的 promise 是被拒绝的。如果任意一个回调返回非 Promise、非 thenable 的立即值,这个值会被用作返回 promise 的完成值。如果完成处理函数返回一个 promise 或 thenable,那么这个值会被展开,并作为返回promise 的决议值。


(4) Promise.all([ .. ]) 和 Promise.race([ .. ])

ES6 Promise API 静态辅助函数 Promise.all([ .. ]) 和 Promise.race([ .. ]) 都会创建一个 Promise 作为它们的返回值。这个 promise 的决议完全由传入的 promise 数组控制。

对 Promise.all([ .. ]) 来说,只有传入的所有 promise 都完成,返回 promise才能完成。如果有任何 promise 被拒绝,返回的主 promise 就立即会被拒绝(抛弃任何其他 promise 的结果)。如果完成的话,你会得到一个数组,其中包含传入的所有 promise 的完成值。对于拒绝的情况,你只会得到第一个拒绝 promise 的拒绝理由值。这种模式传统上被称为门:所有人都到齐了才开门。

对 Promise.race([ .. ]) 来说,只有第一个决议的 promise(完成或拒绝)取胜,并且其决议结果成为返回 promise 的决议。这种模式传统上称为门闩:第一个到达者打开门闩通过。

其实Promise还有内容,由于时间的关系先学习到这里,以后会在继续学习!
原创粉丝点击