欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript之异步 - Promise (二)

程序员文章站 2022-05-08 16:18:29
...

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填充p2
	return v * 2;
} );

// 连接p2
p2.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完成连接的promise
	return 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.JavaScript之异步 - Promise (二)

(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(); //undefined
	return 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。
JavaScript之异步 - Promise (二)

但是如果我们的错误处理函数不是在step3 中,而是在step5中,那么会发生什么呢?我们看一下图示:
JavaScript之异步 - Promise (二)

如你所见,默认拒绝处理函数只是把错误重新抛出,这最终会使得 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已经是下一个事件队列了。
JavaScript之异步 - Promise (二)
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 的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)。

JavaScript之异步 - 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 决议为拒绝,它就会拒绝。
请看例子:
JavaScript之异步 - Promise (二)
一项竞赛需要至少一个“参赛者”。所以,如果你传入了一个空数组,主race([..]) Promise 永远不会决议,而不是立即决议。

(3). 并发迭代

我们考虑一下一个异步的 map(..) 工具。它接收一个数组的值(可以是Promise 或其他任何值),外加要在每个值上运行一个函数(任务)作为参数。 map(..) 本身返回一个 promise,其完成值是一个数组,该数组(保持映射顺序)保存任务执行之后的异步完成值:
Promise.map = function(vals,cb) {
	// 一个等待所有map的promise的新promise
	return Promise.all(
		// 注:一般数组map(..)把值数组转换为 promise数组
		vals.map( function(val){
		// 用val异步map之后决议的新promise替换val
			return 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){
	// 保证这一条本身是一个Promise
	Promise.resolve( pr )
	.then(
		// 提取值作为v
		function(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还有内容,由于时间的关系先学习到这里,以后会在继续学习!
相关标签: js Promise js