目录
Promise.all 和 Promise.race 的区别和使用
什么是Promise
通过如下场景来阐述:
我到快餐店的柜台,点了一个芝士汉堡。通过下订单并付款,我已经发出了一个对某个值(芝士汉堡)的请求。我已经启动了一次交易。但是,通常我不能马上就得到这个汉堡。收银员会交给我某个东西来代替汉堡:一张带有订单号的收据。收据就是一个 IOU
(I owe you
,我欠你的)承诺(promise
),保证了最终我会得到我的汉堡。
所以我得好好保留我的收据,我知道这代表了我未来的汉堡,所以不需要担心, 只是现在我还是很饿!
在等待的过程中,我还可以做点其他的事情,我脑海中已经在想着未来的芝士汉堡了,尽管现在我还没有拿到手。我的大脑之所以可以这么做,是因为它已经把订单号当作芝士汉堡的占位符了。 从本质上讲,这个占位符使得这个值不再依赖时间。这是一个未来值
。
终于,我听到服务员在喊“订单 113”,然后愉快地拿着收据走到柜台,把收据交给收银 员,换来了我的芝士汉堡。换句话说,一旦我需要的值准备好了,我就用我的承诺值(value-promise
)换取这个值本身。
但是,还可能有另一种结果。他们叫到了我的订单号,但当我过去拿芝士汉堡的时候,收银员满是歉意地告诉我:“不好意思,芝士汉堡卖完了。”除了作为顾客对这种情况感到愤怒之外,我们还可以看到未来值
的一个重要特性:它可能成功,也可能失败。
现在值与将来值——-——————
不要小瞧 x + y
。
var x, y = 2;
console.log( x + y ); // NaN
运算 x + y
假定了 x
和 y
都已经设定。也就是说在这里我们假定了 x
和 y
的值都是已决议的,那么试想如果其中一个值未决议(是未来值)就进行计算,那会怎么样?
如果有的语句现在完成,而有的语句将来完成,那就会在程序里引起混乱。例如:如果语句 2
依赖于语句 1
的完成,那么就会有两个输出:要么语句 1
马上完成,一切 顺利执行;要么语句 1
还未完成,语句 2
因此也将会失败。
以x + y
为例,如果它们中的任何一个还没有准备好,那我们就等待两者都准备好。一旦可以就马上执行加运算。下面是通过回调的实现:
function add(getX,getY,cb) {
var x, y;
getX( function(xVal){
x = xVal;
// 两个都准备好了?
if (y != undefined) {
cb( x + y ); // 发送和
}
} );
getY( function(yVal){
y = yVal;
// 两个都准备好了?
if (x != undefined) {
cb( x + y ); // 发送和
}
} );
}
// fetchX() 和fetchY()是同步或者异步函数
add( fetchX, fetchY, function(sum){
console.log( sum ); // 是不是很容易?
} );
上面我们把 x
和 y
都当作未来值来处理。在我们不确定某个值是现在值还是将来值的时候,我们就把它当做将来值来处理,这样可以防止很多意外的发生。说得直白些就是,为了统一处理现在和将来,我们把它们都变成了将来,即所有的操作都成了异步的。
Promise
Promise
值
先来看看,通过 Promise
函数表达 x + y
的例子:
function add(xPromise,yPromise) {
return Promise.all( [xPromise, yPromise] )
.then( function(values){
return values[0] + values[1];
} );
}
// fetchX()和fetchY()返回相应值的promise
add( fetchX(), fetchY() )
.then( function(sum){
console.log( sum );
} );
这样就简单多了。Promise
的决议结果可能是拒绝而不是完成。拒绝值和完成的 Promise
不一样:
完成值总是编程给出的,而拒绝值,通常称为拒绝原因,可能是程序逻辑直接设置的,也可能是从运行异常隐式得出的值
通过 Promise
,调用 then(..)
实际上可以接受两个函数,第一个用于完成情况,第二个用于拒绝情况:
add( fetchX(), fetchY() )
.then(
// 完成处理函数
function(sum) {
console.log( sum );
},
// 拒绝处理函数
function(err) {
console.error( err );
}
);
我们清晰的看见,Promise
采用了分离式回调。Promise
是一种封装和组合未来值的易于复用的机制。
注意: 关于 Promise
需要理解的最强大也最重要的一个概念:一旦 Promise
决议,它就永远保持在这个状态,成为了不变值。(一旦改变不能在改变 承诺者模式)
完成事件
从另外一个角度看待 Promise
的决议:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的 this-then-that
。
假定要调用一个函数执行某个任务,这个函数可能立即完成任务,也可能需要一段时间才能完成。我们只需要知道它什么时候结束,这样就可以进行下一个任务。即我们想要通过某种方式在程序完成的时候得到通知。
侦听某个通知,我们就会想到事件,我们的脑海中可能会出现如下的伪代码:
foo(x) {
// 开始做点可能耗时的工作
}
foo( 42 )
on (foo "completion") {
// 可以进行下一步了!
}
on (foo "error") {
// 啊,foo(..)中出错了
}
当然这样的代码,Javascript
并不提供,更自然的表达方法是:
function foo(x) {
// 开始做点可能耗时的工作
return listener; // 构造一个listener事件通知处理对象来返回
}
var evt = foo( 42 );
evt.on( "completion", function(){
// 可以进行下一步了!
} );
evt.on( "failure", function(err){
// 啊,foo(..)中出错了
} );
bar( evt ); // 让bar(..)侦听foo(..)的完成
这里没有把回调传给 foo(..)
,而是返回一个名为 evt
的事件注册对象,由它来接受回调。此处的反转显而易见,我们通过反转再反转,拿回了对代码的控制权,即调用代码将控制权反转给第三方,再从第三方那里反转回来。对控制反转的恢复实现了更好的关注点分离,即bar
不需要关注foo(...)
的调用细节,foo(..)
也不需要关注bar
是否存在。从本质上说,evt
对象就是分离的关注点之间一个中立的第三方协商机制。
Promise
“事件”
其实上面的事件侦听对象 evt
就是 Promise
的一个模拟。foo(..)
与 bar(..)
的内部实现或许如下
function foo(x) {
// 可是做一些可能耗时的工作
// 构造并返回一个promise
return new Promise( function(resolve,reject){
// 最终调用resolve(..)或者reject(..)
// 这是这个promise的决议回调
} );
}
function bar(fooPromise) {
// 侦听foo(..)完成
fooPromise.then(
function(){
// foo(..)已经完毕,所以执行bar(..)的任务
},
function(){
// 啊,foo(..)中出错了!
}
);
}
注意: 传入Promise
的函数会立即执行,不会像 then(..)
中的回调一样异步延迟
另一种实现方式:
function bar() {
// foo(..)肯定已经完成,所以执行bar(..)的任务
}
function oopsBar() {
// 啊,foo(..)中出错了,所以bar(..)没有运行
}
// 对于baz()和oopsBaz()也是一样
var p = foo( 42 );
p.then( bar, oopsBar );
p.then( baz, oopsBaz );
注意: p.then( .. ).then( .. )
与 p.then(..); p.then(..);
是两个意义,前者p
决议后调用then
,因为.then
返回的总是Promise
,所以才能支持链式调用,此时第二个.then
用的是第一个.then
返回的Promise
的决议值,而后者用的都是p
的决议值。
具有 then
方法的鸭子类型
判断类似于 Promise
的值是否是真正的 Promise
很重要,多见于 Promise.resolve()
对于 Promise
和 thenable
的展开。 thenable
类似于 Promise
,指任何具有 then(..)
方法的对象和函数。
注意: 对象的原型链上若具有 then(..)
方法,那么这个对象也会被识别为 thenable
。
这里主要讲对thenable
的类型检查。
根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查一般用术语鸭子类型来表示——“如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是只鸭子”。
对 thenable
值的鸭子类型检测就大致如下:
if (
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
) {
// 假定这是一个thenable!
}
else {
// 不是thenable
}
注意
如果有其它代码无意或恶意地给Object.prototype、Array.prototype或者其它原生原型添加了then方法,也会造成灾难。不过鸭子类型有时候还是有用的,只是要小心鸭子类型把不是Promise的值误判为Promise的情况。
Promise
信任问题
Promise
的特性就是专门用来为回调编码的信任问题提供一个有效的可复用的答案。
调用过早
即使是立即完成的 Promise
也无法被同步观察到。即对一个 Promise
调用 then(..)
的时候,即使这个 Promise
已经决议,提供给then(..)
的回调也总会被异步调用(微队列)。所以 Promise
不存在调用过早这个问题。(.then异步微任务)
调用过晚
Promise
创建对象调用 resolve(..)
或 reject(..)
时,这个 Promise
的 then(..)
注册的观察回调就会被自动调度。可以确信,这些被调度的回调在下一个异步事 件点上一定会被触发。所以也不存在调用过晚的问题。
注意: 当 Promise
决议后,其上所有的通过 then(..)
注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。
举个🌰:
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// 结果:A B C
这里的 "C"
无法打断或抢占"B"
。 下面这样写又是另一种结果:
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} )
.then( function(){
console.log( "B" );
} );
// 结果:A C B
这里就要注意到 p.then( .. ).then( .. )
与 p.then(..); p.then(..);
是两个意义
Promise
调度技巧
如果两个 promise p1
和 p2
都已经决议,那么 p1.then(..); p2.then(..)
应该最终会先调用 p1
的回调,然后是 p2
的那些。但还有一些微妙的场景可能不是这样的:
var p3 = new Promise( function(resolve,reject){
resolve( "B" );
} );
var p1 = new Promise( function(resolve,reject){
resolve( p3 );
} );
p2 = new Promise( function(resolve,reject){
resolve( "A" );
} );
p1.then( function(v){
console.log( v );
} );
p2.then( function(v){
console.log( v );
} );
// 结果:A B
不易理解的点就在resolve( p3 )
,规定的行为是把 p3
展开到 p1
,但是注意是异步地展开。
注意: new Promise((resolve)=>{resolve(val)})
与 Promise.resolve(val)
并不一定等价
- 当
val
是一个Promise
实例时,resolve(val)
是异步展开,而Promise.resolve
将不做任何修改、原封不动地返回这个实例 - 当
val
是一个thenable
对象时,都采取异步展开 - 当
val
是其他值时,两种方式等价
这里针对前两种情况给出测试代码(chrome下
):
Promise.resolve
传入 Promise
实例 是异步展开
resolve()
传入 Promise
实例,chrome
下回调会被推迟两个时序
原因:new Promise(r => r(v))
里浏览器会创建一个 PromiseResolveThenableJob
去处理这个 Promise
实例,也就是所谓的展开
new Promise(resolve => {
resolve(1);
Promise.resolve(
new Promise(function(resolve, reject){
console.log(2);
resolve(3)
})
).then(t => console.log(t))
console.log(4);
}).then(t => console.log(t));
console.log(5);
// 结果: 2 4 5 3 1
new Promise((resolve, reject) => {
console.log(1);
resolve(Promise.resolve()); // 直接用Promise.resolve()生成实例
}).then(() => {
console.log(2);
});
new Promise(function(resolve) {
console.log(3);
resolve();
}).then(function() {
console.log(4);
}).then(function() {
console.log(5);
}).then(function() {
console.log(6);
});
// 结果:1 3 4 5 2 6
Promise.resolve
传入 thenable
对象(promise)将不做任何修改、原封不动地返回这个实例
resolve()
传入thenable
对象,chrome
下回调被推迟一个时序
new Promise(resolve => {
resolve(1);
Promise.resolve({
then: function(resolve, reject){
console.log(2);
resolve(3)
}
}).then(t => console.log(t))
console.log(4);
}).then(t => console.log(t));
console.log(5);
// 结果: 4 5 2 1 3
new Promise((resolve, reject) => {
console.log(1);
resolve({
then: function(resolve, reject){
resolve()
}
});
}).then(() => {
console.log(2);
});
new Promise(function(resolve) {
console.log(3);
resolve();
}).then(function() {
console.log(4);
}).then(function() {
console.log(5);
}).then(function() {
console.log(6);
});
// 结果:1 3 4 2 5 6
这些细微的差别,确实很让人头疼。要避免这样的细微区别带来的噩梦,你永远都不应该依赖于不同 Promise
间回调的顺序和调度。
- - 执行函数中执行resolve方法会修改为成功状态
- - 执行函数中执行rejecte方法或者有执行代码==报错==,会修改为失败状态
- - then方法无论执行哪种状态的回调函数,都会默认return一个新的Promise实例resolve状态的对象,默认值为undefined,通过return res的方式可以给下一次的函数中传值。
- - 如果想让then方法的回调函数返回状态为rejected的promise实例,就需要return new rejected对象,或者throw一个错误。
回调未调用
首先,没有任何东西(甚至 JavaScript
错误)能阻止 Promise
向你通知它的决议,Promise
在决议时总是会调用完成回调和拒绝回调中的一个。
如果 Promise
本身永远不被决议,即使这样,Promise 也提供了解决方案:
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(val => console.log(val)) // 及时完成
.catch(err => console.error(err)); // 超时
调用次数过少或过多
根据定义,回调被调用的正确次数应该是 1。Promise
的定义方式使得它只能被决议一次。
注意:
如果出于某种原因,
Promise
创建代码试图调用resolve(..)
或reject(..)
多次,或者试图两者都调用,那么这个Promise
将只会接受第一次决议,并默默地忽略任何后续调用。
由于Promise
只能被决议一次,所以任何通过then(..)
注册的(每个)回调就只会被调用一次。如果你把同一个回调注册了不止一次(比如p.then(f); p.then(f);
),那它被调用的次数就会和注册次数相同。
未能传递参数 / 环境值
- 如果你没有用任何值显式决议,那么这个值就是
undefined
,这是JavaScript
常见的处理方 式。 - 如果要传递多个值,你就必须要把它们封装在单个值中传递,比如通过一个数组或对象。
吞掉错误或异常
如果在 Promise
的创建过程中或在查看其决议结果过程中的任何时间点上出现了一个 JavaScript
异常错误,那这个异常就会被捕捉,并且会使这个 Promise
被拒绝。
如果在处理异常的过程中再次发生异常,它看起来好像被吞掉了,其实你并没有捕获。
是可信任的 Promise
吗?
Promise
并没有完全摆脱回调,它只是改变了传递回调的位置。
我们并不是把回调传递给
foo(..)
,而是从foo(..)
得到某个东西(外观上看是一个真正的Promise
),然后把回调传给这个东西
Promise.resolve(..)
提供了可信任的 Promise
封装工具,可以链接使用。
链式流
Promise
的两个固有行为特性:
- 调用
Promise
的then(..)
会自动创建一个新的Promise
从调用返回- 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)
Promise
就相应地决议- 如果完成或拒绝处理函数返回一个
Promise
,它将会被展开,这样一来,不管它的决议 值是什么,都会成为当前then(..)
返回的链接Promise
的决议值
注意: 从完成(resolve()
)处理函数返回 thenable
或者 Promise
的时候会发生展开,但从(拒绝reject()
)处理函数返回 thenable
或者 Promise
时不会发生展开(经测试得出)。
举个🌰:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 创建一个promise并将其返回
return new Promise( function(resolve,reject){
resolve( v * 2 );
} );
} )
.then( function(v){
console.log( v ); // 42
} );
你可以认为返回的Promise
实例覆盖了原来默认返回的Promise
实例。再来看看reject()
:
new Promise(function(resolve,reject){
reject(
Promise.resolve(1)
)
})
.then(val=>console.log(2,val))
.catch(err=>console.log(3,err ))
// 结果:3 Promise {<resolved>: 1}
可以看到 err
是一个 Promise
,足以证明 reject()
中并不会发生展开,当然你也可以去试试传入 thenable
,也是同样的结果。
- 如果你调用
promise
的then(..)
,并且只传入一个完成处理函数,一个默认拒绝处理函数 就会顶替上来:
var p = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = p.then(
function fulfilled(){
// 永远不会达到这里
}
// 假定的拒绝处理函数,如果省略或者传入任何非函数值
// function(err) {
// throw err;
// }
);
- 如果没有给
then(..)
传递一个适当有效的函数作为完成处理函数参数,还是会有作为替代 的一个默认处理函数:
var p = Promise.resolve( 42 );
p.then(
// 假设的完成处理函数,如果省略或者传入任何非函数值
// function(v) {
// return v;
// }
null,
function rejected(err){
// 永远不会到达这里
}
)
Promise API
Promise 用法
function foo(name) {
return new Promise((resolve, reject) => {
name ? resolve({ name }) : reject({ error: 'error' });
})
}
foo('ProsperLee')
.then(
// 成功
value => {
console.log(value); // ProsperLee
},
// 失败 - 优先调用
reason => {
console.log('error', reason);
}
)
// 失败
.catch(reason => {
console.log('reason', reason);
})
// 完成
.finally(_ => {
console.log('Hello ProsperLee');
})
foo()
.then(
// 成功
value => {
console.log(value);
},
// 失败 - 优先调用
reason => {
console.log('error', reason); // error {error: 'error'}
}
)
// 失败
.catch(reason => {
console.log('reason', reason);
})
// 完成
.finally(_ => {
console.log('Hello Error');
})
// 最终输出结果
// ProsperLee
// error {error: 'error'}
// Hello ProsperLee
// Hello Error
Promise.all 和 Promise.race 的区别和使用
function foo(name) {
return new Promise((resolve, reject) => {
name ? resolve({ name }) : reject({ error: 'error' });
})
}
function bar(name) {
return new Promise((resolve, reject) => {
setTimeout(_ => {
name ? resolve({ name }) : reject({ error: 'error' });
}, 3000);
})
}
// 等待然后返回两个函数成功的返回值
Promise.all([foo('Lee'), bar('Tom')])
.then(values => {
// [{name: 'Lee'}, {name: 'Tom'}]
console.log(values);
}, reason => {
console.log(reason);
})
// 等待然后返回函数错误的返回值
Promise.all([foo('Lee'), bar()])
.then(values => {
console.log(values);
}, reason => {
// {error: 'error'}
console.log(reason);
})
// 返回出结果快的那个函数的返回值,不论成功或失败
Promise.race([foo(), bar('Tom')])
.then(value => {
console.log(value);
}, reason => {
// {error: 'error'}
console.log(reason);
})
Promise - finally
function foo(name) {
return new Promise((resolve, reject) => {
setTimeout(_ => {
name ? resolve({ name }) : reject({ error: 'error' });
}, 3000);
})
}
foo('Lee')
.finally(_ => {
console.log('A');
})
.then(res => {
console.log(res);
return res;
})
.finally(_ => {
console.log('B');
})
.then(res => {
console.log(res);
})
.finally(_ => {
console.log('C');
})
// 最终输出结果:
// A
// {name: 'Lee'}
// B
// {name: 'Lee'}
// C
Promise
局限性
顺序错误处理
Promise
链中的错误很容易被无意中默默忽略掉,当然最佳实践就是Promise
链的最后总以一个 catch(..)
结束
单一值
Promise
只能有一个完成值或一个拒绝理由。一般的建议是构造一个值封装(比如一个对象或数组)来保持多个信息。
单决议
还有很多异步的情况适合另一种模式——一种类似于事件和 / 或数据流的模式。
设想场景:你可能要启动一系列异步步骤以响应某种可能多次发生的激励(就像是事件),比如按钮点击。这样可能不会按照你的期望工作:
// click(..)把"click"事件绑定到一个DOM元素
// request(..)是前面定义的支持Promise的Ajax
var p = new Promise( function(resolve,reject){
click( "#mybtn", resolve );
} );
p.then( function(evt){
var btnID = evt.currentTarget.id;
return request( "http://some.url.1/?id=" + btnID );
} )
.then( function(text){
console.log( text );
} );
只有在按钮点击一次的情况下,这种方式才能工作。如果这个按钮被点击了第二次的话,promise p
已经决议,因此第二个 resolve(..)
调用就会被忽略。
解决办法:为每个事件的发生创建一整个新的 Promise
链。如下:
click( "#mybtn", function(evt){
var btnID = evt.currentTarget.id;
request( "http://some.url.1/?id=" + btnID )
.then( function(text){
console.log( text );
} );
} );
这个设计在某种程度上破坏了关注点与功能分离(SoC
)的思想。
惯性
这里给到一个基于回调的代码转化为基于 Promise
的代码的范例:
if (!Promise.wrap) {
Promise.wrap = function(fn) {
return function() {
var args = [].slice.call( arguments );
return new Promise( function(resolve,reject){
fn.apply(
null,
args.concat( function(err,v){
if (err) {
reject( err );
}
else {
resolve( v );
}
} )
);
} );
};
};
}
// 使用
var request = Promise.wrap( ajax );
request( "http://some.url.1/" )
.then( .. )
我们见识到了闭包的强大之处。闭包是 JS
最强大的特性,没有之一。
无法取消的 Promise
一旦创建了一个 Promise
并为其注册了完成和 / 或拒绝处理函数,如果出现某种情况使得 这个任务悬而未决的话,你也没有办法从外部停止它的进程,一种选择是侵入式地定义你自己的决议回调,当然这很丑陋。如下:
var OK = true;
var p = foo( 42 );
Promise.race( [
p,
timeoutPromise( 3000 )
.catch( function(err){
OK = false;
throw err;
} )
] )
.then(
doSomething,
handleError
);
p.then( function(){
if (OK) {
// 只在没有超时情况下才会发生 :)
}
} );
Promise
性能
更多的工作,更多的保护,这些意味着 Promise
与不可信任的裸回调相比会更慢一些。 但我们不要耿耿于 Promise
微小的性能损失而无视它提供的所有优点, 虽然 Promise
稍慢一些,但是作为交换,得到的是大量内建的可信任性、对 Zalgo
的避免以及 可组合性。
😏😏😏 Promise
非常好,请使用。它们解决了我们因只用回调的代码而备受困扰的控制反转问题。