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中)。
先看一段代码
但是,如果必须创建一个临时变量 p2(或 p3 等),还是有一点麻烦的。谢天谢地,我们很容易把这些链接到一起:
我们再看一下向封装的 promise 引入异步
请看下面图片中没有返回值时,下一个then接受的参数为undefined.
不管从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.
(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。但是如果我们的错误处理函数不是在step3 中,而是在step5中,那么会发生什么呢?我们看一下图示:
如你所见,默认拒绝处理函数只是把错误重新抛出,这最终会使得 p2(链接的 promise)
用同样的错误理由拒绝。从本质上说,这使得错误可以继续沿着 Promise 链传播下去,直到遇到显式定义的拒绝处理函数,也就是我们看到的step5中的处理函数,大家想一下,如果没有错误处理函数的话,就会直接在浏览器控制台报错,由此可见,错误处理函数的重要性。默认拒绝处理函数也可能默默丢失掉,浏览器都检测不到,这个的话认识到就行了。
你可以看到,默认的完成处理函数只是把接收到的任何传入值传递给下一个步骤
( Promise)而已。
Note:
then(null,function(err){ .. }) 这个模式——只处理拒绝(如果有的话),但又把完成值传递下去——有一个缩写形式的 API: catch(function(err)
{ .. })。下一小节会详细介绍 catch(..).
• 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。
• 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise 就相应地决议。
• 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议
值是什么,都会成为当前 then(..) 返回的链接 Promise 的决议值。
尽管链式流程控制是有用的,但是对其最精确的看法是把它看作 Promise 组合到一起的一个附加益处,而不是主要目的。正如前面已经多次深入讨论的, Promise 规范化了异步,并封装了时间相关值的状态,使得我们能够把它们以这种有用的方式链接到一起。
当然,相对于前面讨论的回调的一团乱麻,链接的顺序表达( this-then-this-then-this...)已经是一个巨大的进步。但是,仍然有大量的重复样板代码( then(..) 以及 function(){ ... })。在后面的文章中(有机会的话),我们将会看到在顺序流程控制表达方面提升巨大的优美模式,通过生成器实现。
对多数开发者来说,错误处理最自然的形式就是同步的 try..catch 结构。遗憾的是,它只能是同步的,无法用于异步代码模式:
如你所见,默认拒绝处理函数只是把错误重新抛出,这最终会使得 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. 错误处理
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的新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】JavaScript异步操作系列(3)——Promise【1】
-
JavaScript编程开发之js异步编程教程
-
javascript之DOM技术(二) JavaScriptIECSS搜索引擎HTML
-
前端笔记知识点整合之JavaScript(二)
-
javascript异步编程代码书写规范Promise学习笔记_javascript技巧
-
Javascript学习笔记二 之 变量_基础知识
-
Javascript 学习笔记之 对象篇(二) : 原型对象_基础知识
-
ES6 异步编程解决方案 之 Promise 对象
-
JavaScript异步编程Promise模式的6个特性_javascript技巧
-
突袭HTML5之Javascript API扩展1—Web Worker异步执行及相关概述