转载请注明出处
原文连接 http://blog.huanghanlian.com/article/5c9524514db639147ebe3300
前言
在工作中,经常用到Promise,一直以来都没有好好的整理。一直停留在会用阶段。想通过此篇文章。能够让我对Promise理解的更深些。
学习目的:
- 学习Promise相关内容,能熟练使用Promise模式并进行测试
- 学习Promise适合什么、不适合什么
- 以Promises为基础进行学习,帮助后期对学习Generator和开发nodeJs中得到好的帮助
一 什么是Promise
什么是Promise
Promise是抽象异步处理对象以及对其进行各种操作的组件。
Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。
js中最原始的异步解决方案就是回调函数
asyncOperation1(data1,function (result1) {
asyncOperation2(data2,function(result2){
asyncOperation3(data3,function (result3) {
asyncOperation4(data4,function (result4) {
//do something
})
})
})
})
这种代码风格不仅可读性很差,而且在团队中很不好维护。
而Promise可以极大优化异步代码,并且能够使代码逻辑清晰。
asyncOperation1(data)
.then(function (data1) {
return asyncOperation2(data1)
})
.then(function(data2){
return asyncOperation3(data2)
})
.then(function(data3){
return asyncOperation(data3)
})
Promise简介
Promise工作流程
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.floor(Math.random() * 10);
random > 5
?resolve('success')
: reject('error')
}, 16);
});
}
asyncFunction()
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
});
promise构造器 只接收一个参数,该参数被称为执行器(executor)的函数。该函数会被传递两个参数(方法),一个叫做resolve,另一个叫做reject。
resolve函数在成功时调用,reject函数在失败时被调用。并且resolve和reject只能被使用一次,如果之后还有resolve和reject也不会被执行了,有点儿类似于return,但是不同点在于,其他代码还会被照常执行。
new Promise构造器之后,会返回一个promise对象
对于这个promise
对象,我们调用它的then
方法来设置resolve
后的回调函数, catch
方法来设置发生错误时reject
的回调函数。
以上promise
对象会在setTimeout
之后的16ms时。随机模拟成功或者失败的调用
如果只想处理错误方法
asyncFunction()
.then(null,(error) => {
console.log(error);
})
//同等于
asyncFunction()
.catch((error) => {
console.log(error);
})
如果不使用 catch 方法只使用 then 方法的话,下面的代码也能完成相同的工作。
asyncFunction()
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
});
// 等同于
asyncFunction()
.then((value) => {
console.log(value);
},(error) => {
console.log(error);
})
但是 通过catch 处理错误是大家比较常用的。因为这样的书写方式,以及在阅读上会比较清楚。
也可以.then
很多次最后再来统一来做个.catch
asyncFunction()
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
});
还有一个好处就是说,假设你的 .then(onFulfilled)
onFulfilled成功处理函数中抛了异常。
在它后面的.catch
能获取到。
而如果在在.then(onFulfilled, onRejected)
这样写法中。onRejected
错误处理函数是无法获取的。
asyncFunction()
.then((value) => {
console.log(value); //success
throw new Error('err 发生了意外');
})
.catch((error) => {
console.log(error); //Error: err 发生了意外
});
asyncFunction()
.then((value) => {
console.log(value); //success
throw new Error('err 发生了意外');
},(error) => {
//在这里不能捕获到.then抛得错误
console.log(error);
})
Promise的状态
用 new Promise
实例化的promise
对象有以下三个状态。
"has-resolution" - Fulfilled
resolve(成功)时。此时会调用 onFulfilled
"has-rejection" - Rejected
reject(失败)时。此时会调用 onRejected
"unresolved" - Pending
既不是resolve也不是reject的状态。也就是promise对象刚被创建后的初始化状态等
编写Promise代码
创建promise对象
创建promise对象的步骤。
- new Promise(fn) 返回一个promise对象
- 在 fn 中指定异步等处理
- 处理结果正常的话,调用 resolve(处理结果值)
- 处理结果错误的话,调用 reject(Error对象)
按这个流程我想通过Promise
封装一个异步处理的原生请求方法
创建XHR的promise对象
首先,创建一个用Promise把XHR处理包装起来的名为 getURL 的函数。
function getURL(URL) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function() {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function() {
reject(new Error(req.statusText));
};
req.send();
});
}
// 运行示例
var URL = "http://blog.huanghanlian.com/";
getURL(URL) // => 返回promise对象
.then((value) => { // promise对象被 resolve 时的处理(onFulfilled)
console.log(value);
})
.catch((error) => {
console.error(error); // promise对象被 reject 时的处理(onRejected)
});
getURL
只有在通过XHR取得结果状态为200时才会调用 resolve
- 也就是只有数据取得成功时,而其他情况(取得失败)时则会调用reject
方法。
resolve(req.responseText)
在response
的内容中加入了参数。 resolve
方法的参数并没有特别的规则,基本上把要传给回调函数参数放进去就可以了。 (then
方法可以接收到这个参数值)
发生错误时要像这样 reject(new Error(req.statusText))
; ,创建一个Error
对象后再将具体的值传进去。 传给 reject
的参数也没有什么特殊的限制,一般只要是Error
对象(或者继承自Error
对象)就可以。
二 实战Promise
Promise.resolve
一般情况下我们都会使用 new Promise() 来创建promise对象,但是除此之外我们也可以使用其他方法。
new Promise的快捷方式
静态方法 Promise.resolve(value)
可以认为是 new Promise()
方法的快捷方式。
Promise.resolve(42);
可以认为是以下代码的语法糖。
//例子1
let resInstance=new Promise(function(resolve){
resolve(42);
});
resInstance.then((res)=>{
console.log(res);//42
})
// 同等于
//例子2
let resStatic=Promise.resolve(42);
resStatic.then((res)=>{
console.log(res);//42
})
方法 Promise.resolve(value); 的返回值也是一个promise对象,所以我们可以像例子2那样接着对其返回值进行 .then 调用。
在例子2中的resolve(42);
会让这个promise
对象立即进入确定(即resolved)状态,
并将 42 传递给后面then里所指定的 onFulfilled
函数。
Promise.resolve
方法的参数分成四种情况。
(1)参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
function getURL(URL) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function() {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function() {
reject(new Error(req.statusText));
};
req.send();
});
}
//getURL 是 Promise 实例
Promise.resolve(getURL("http://www.huanghanlian.com/data_location/list.json"))
.then((res)=>{
console.log(res);
})
.catch((error)=>{
console.log('捕获错误')
console.log(error);
})
//如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
(2)参数是一个thenable对象
thenable
对象指的是具有then
方法的对象,比如下面这个对象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve
方法会将这个对象转为 Promise
对象,然后就立即执行thenable
对象的then
方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
上面代码中,thenable
对象的then
方法执行后,对象p1
的状态就变为resolved
,从而立即执行最后那个then
方法指定的回调函数,输出 42
。
(3)参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then
方法的对象,则Promise.resolve
方法返回一个新的 Promise
对象,状态为resolved
。
const p = Promise.resolve('Hello');
p.then(function(s) {
console.log(s) // Hello
});
上面代码生成一个新的 Promise
对象的实例p
。由于字符串Hello
不属于异步操作(判断方法是字符串对象不具有 then
方法),返回 Promise
实例的状态从一生成就是resolved
,所以回调函数会立即执行。Promise.resolve
方法的参数,会同时传给回调函数。
(4)不带有任何参数
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的 Promise
对象。
所以,如果希望得到一个 Promise
对象,比较方便的方法就是直接调用Promise.resolve
方法。
const p = Promise.resolve();
p.then(() => {
// 做我该做的事情...
});
上面代码的变量p
就是一个 Promise
对象。
需要注意的是,立即resolve
的 Promise
对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
setTimeout(function() {
console.log('three');
}, 0);
Promise.resolve().then(function() {
console.log('two');
});
console.log('one');
// one
// two
// three
上面代码中,setTimeout(fn, 0)
在下一轮“事件循环”开始时执行,Promise.resolve()
在本轮“事件循环”结束时执行,console.log('one')
则是立即执行,因此最先输出。
Promise.reject
Promise.reject(error)
是和 Promise.resolve(value)
类似的静态方法,是 new Promise()
方法的快捷方式。
比如 Promise.reject(new Error("出错了"))
就是下面代码的语法糖形式。
Promise.reject(new Error("出错了"))
.catch(error => {
console.log(error)
})
//同等于
new Promise(function(resolve, reject) {
reject(new Error("出错了"));
})
.catch(error => {
console.log(error)
})
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代码中,Promise.reject
方法的参数是一个thenable
对象,执行以后,后面catch
方法的参数不是reject
抛出的“出错了”这个字符串,而是thenable
对象。
Promise只能进行异步操作?
在使用 Promise.resolve(value)
等方法的时候,如果promise
对象立刻就能进入resolve
状态的话,那么 .then
里面指定的方法就是同步调用的呢?
实际上, .then
中指定的方法调用是异步进行的。
var promise = new Promise(function(resolve) {
console.log("inner promise"); // 1
resolve(42);
});
promise.then(function(value) {
console.log(value); // 3
});
console.log("outer promise"); // 2
//inner promise
//outer promise
//42
从上面代码执行的结果看出
js是会按照文件由上而下执行,所以最开始执行Promise
实例。
给这个Promise
实例传入执行者函数,它是同步的,所以会先打印1
紧接着立刻执行了resolve(42)
方法,这时候 promise
对象的已经变为确定状态,FulFilled
被设置为了 42
当promise.then
中注册了成功后的回调。这块是问题的所在。
由于 promise.then
执行的时候promise
对象已经是确定状态,从程序上说对回调函数进行同步调用也是行得通的。
但是即使在调用 promise.then
注册回调函数的时候promise
对象已经是确定的状态,Promise
也会以异步的方式调用该回调函数,这是在Promise
设计上的规定方针。
因此 2
会最先被调用,最后才会调用回调函数 3
。为什么要对明明可以以同步方式进行调用的函数,非要使用异步的调用方式呢?
同步调用和异步调用同时存在导致的混乱
其实在Promise
之外也存在这个问题,这里我们以一般的使用情况来考虑此问题。
这个问题的本质是接收回调函数的函数,会根据具体的执行情况,可以选择是以同步
还是异步
的方式对回调函数进行调用。
我们以 onReady(fn) 为例进行说明,这个函数会接收一个回调函数进行处理。
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
fn();
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function() {
console.log('DOM fully loaded and parsed');
});
上面的代码会根据执行时DOM是否已经装载完毕来决定是对回调函数进行同步调用还是异步调用。
如果在调用onReady之前DOM已经载入的话
对回调函数进行同步调用
如果在调用onReady之前DOM还没有载入的话
通过注册 DOMContentLoaded 事件监听器来对回调函数进行异步调用
因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的log消息顺序也会不同。
为了解决这个问题,我们可以选择统一使用异步调用的方式。
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
setTimeout(fn, 0);
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function() {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
在日常开发中
- 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。
- 如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不
符,可能带来意料之外的后果。 - 对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。
- 如果想在将来某时刻调用异步回调函数的话,可以使用 setTimeout
等异步API。
前面我们看到的 promise.then
也属于此类,为了避免上述中同时使用同步、异步调用可能引起的混乱问题,Promise
在规范上规定 Promise
只能使用异步调用方式 。
最后,如果将上面的 onReady 函数用Promise重写的话,代码如下面所示。
function onReadyPromise() {
return new Promise(function(resolve, reject) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', resolve);
}
});
}
onReadyPromise().then(function() {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
由于Promise
保证了每次调用都是以异步方式进行的,所以我们在实际编码中不需要调用 setTimeout
来自己实现异步调用。
Promise.then
在Promise
里可以将任意个方法连在一起作为一个方法链(method chain)。
promise可以写成方法链的形式
aPromise.then(function taskA(value) {
// task A
}).then(function taskB(vaue) {
// task B
}).catch(function onRejected(error) {
console.log(error);
})
如果把在 then
中注册的每个回调函数称为task的话,那么我们就可以通过Promise
方法链方式来编写能以taskA → task B 这种流程进行处理的逻辑
promise chain(promise 链)
写一个较长的链式promise
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
上面代码中的promise chain
的执行流程,如果用一张图来描述一下的话,像下面的图那样。
在 上面代码 中,没有为 then 方法指定第二个参数(onRejected),也可以像下面这样来理解。
then
注册onFulfilled时的回调函数
catch
注册onRejected时的回调函数
再看一下 上面的流程图 的话,我们会发现 Task A 和 Task B 都有指向 onRejected 的线出来。
这些线的意思是在 Task A 或 Task B 的处理中,在下面的情况下就会调用 onRejected 方法。
- 发生异常的时候
- 返回了一个Rejected状态的promise对象
Promise中的处理习惯上都会采用 try-catch
的风格,当发生异常的时候,会被 catch
捕获并被由在此函数注册的回调函数进行错误处理。
另一种异常处理策略是通过 返回一个Rejected
状态的promise
对象 来实现的,这种方法不通过使用 throw
就能在promise chain
中对 onRejected
进行调用。
此外在promise chain
中,由于在 onRejected
和 Final Task
后面没有 catch
处理了,因此在这两个Task
中如果出现异常的话将不会被捕获
Task A产生异常的例子
function taskA() {
console.log("Task A");
throw new Error("throw Error @ Task A")
}
function taskB() {
console.log("Task B"); // 不会被调用
}
function onRejected(error) {
console.log(error); // => "throw Error @ Task A"
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
执行这段代码会发现 Task B 是不会被调用的。
使用reject而不是throw
Promise
的构造函数,以及被 then
调用执行的函数基本上都可以认为是在 try...catch
代码块中执行的,所以在这些代码中即使使用 throw
,程序本身也不会因为异常而终止。
如果在Promise
中使用 throw
语句的话,会被 try...catch
住,最终promise
对象也变为Rejected
状态。
var promise = new Promise(function(resolve, reject) {
throw new Error("message");
});
promise.catch(function(error) {
console.error(error); // => "message"
});
代码像这样其实运行时倒也不会有什么问题,但是如果想把 promise
对象状态 设置为Rejected
状态的话,使用 reject
方法则更显得合理
var promise = new Promise(function(resolve, reject) {
reject(new Error("message"));
});
promise.catch(function(error) {
console.error(error); // => "message"
})
其实也可以这么来考虑,在出错的时候我们并没有调用 throw
方法,而是使用了reject
,那么给 reject
方法传递一个Error类型的对象也就很好理解了。
promise chain 中如何传递参数
前面例子中的Task都是相互独立的,只是被简单调用而已。
这时候如果 Task A 想给 Task B 传递一个参数该怎么办呢?
答案非常简单,那就是在 Task A 中 return
的返回值,会在 Task B 执行时传给它。
function increment(value) {
return value + 1;
}
function doubleUp(value) {
return value * 2;
}
function output(value) {
console.log(value); // => (1 + 1) * 2
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error) {
// promise chain中出现异常的时候会被调用
console.error(error);
});
这段代码的入口函数是 Promise.resolve(1);
,整体的promise chain
执行流程如下所示。
- Promise.resolve(1); 传递 1 给 increment 函数
- 函数 increment 对接收的参数进行 +1 操作并返回(通过 return )
- 这时参数变为2,并再次传给 doubleUp 函数
- 最后在函数 output 中打印结果
每个方法中 return
的值不仅只局限于字符串或者数值类型,也可以是对象或者promise
对象等复杂类型。
return
的值会由 Promise.resolve(return的返回值);
进行相应的包装处理,因此不管回调函数中会返回一个什么样的值,最终 then
的结果都是返回一个新创建的promise
对象。
也就是说, Promise#then
不仅仅是注册一个回调函数那么简单,它还会将回调函数的返回值进行变换,创建并返回一个promise
对象。
Promise.catch
实际上 Promise#catch
只是 promise.then(undefined, onRejected);
方法的一个别名而已。 也就是说,这个方法用来注册当promise
对象状态变为Rejected
时的回调函数。
IE8的问题
上面的这张图,是下面这段代码在使用 polyfill25 的情况下在个浏览器上执行的结果
Promise#catch的运行结果
var promise = Promise.reject(new Error("message"));
promise.catch(function(error) {
console.error(error);
});
如果我们在各种浏览器中执行这段代码,那么在IE8及以下版本则会出现 identifier notfound 的语法错误
实际上这和 catch 是ECMAScript的 保留字有关
在ECMAScript 3
中保留字是不能作为对象的属性名使用的。 而IE8
及以下版本都是基于ECMAScript 3
实现的,因此不能将 catch 作为属性来使用,也就不能编写类似promise.catch()
的代码,因此就出现了 identifier not found
这种语法错误了。
解决Promise#catch标识符冲突问题
var promise = Promise.reject(new Error("message"));
promise["catch"](function(error) {
console.error(error);
});
或者不单纯的使用 catch ,而是使用 then 也是可以避免这个问题的。
使用Promise#then代替Promise#catch
var promise = Promise.reject(new Error("message"));
promise.then(undefined, function(error) {
console.error(error);
});
很多压缩工具自带了将 promise.catch 转换为 promise["catch"] 的功能, 所以可能不经意之间已经解决这个问题。
如果需要支持IE8及以下版本的浏览器的话,那么一定要将这个 catch 问题牢记在心中。
每次调用then都会返回一个新创建的promise对象
从代码上看,aPromise.then(...).catch(...)
像是针对最初的 aPromise
对象进行了一连串的方法链调用。
然而实际上不管是 then
还是 catch
方法调用,都返回了一个新的promise
对象。
var aPromise = new Promise(function(resolve) {
resolve(100);
});
var thenPromise = aPromise.then(function(value) {
console.log(value);
});
var catchPromise = thenPromise.catch(function(error) {
console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise); // => true
===
是严格相等比较运算符,我们可以看出这三个对象都是互不相同的,这也就证明了 then
和 catch
都返回了和调用者不同的promise
对象。
知道了 then
方法每次都会创建并返回一个新的promise
对象的话,那么我们就应该不难理解下面代码中对 then
的使用方式上的差别了。
// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function(resolve) {
resolve(100);
});
aPromise.then(function(value) {
return value * 2;
});
aPromise.then(function(value) {
return value * 2;
});
aPromise.then(function(value) {
console.log("1: " + value); // => 100
})
// vs
// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function(resolve) {
resolve(100);
});
bPromise.then(function(value) {
return value * 2;
}).then(function(value) {
return value * 2;
}).then(function(value) {
console.log("2: " + value); // => 100 * 2 * 2
});
第1
种写法中并没有使用promise
的方法链方式,这在Promise
中是应该极力避免的写法。这种写法中的 then
调用几乎是在同时开始执行的,而且传给每个 then
方法的value
值都是 100
。
第2
中写法则采用了方法链的方式将多个 then
方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then
的顺序执行,并且传给每个 then
方法的 value
的值都是前一个promise
对象通过 return
返回的值。
下面是一个由方法1中的 then 用法导致的比较容易出现的很有代表性的反模式的例子。
then 的错误使用方法
function badAsyncCall() {
var promise = Promise.resolve();
promise.then(function() {
// 任意处理
return newVar;
});
return promise;
}
这种写法有很多问题,首先在 promise.then
中产生的异常不会被外部捕获,此外,也不能得到 then
的返回值,即使其有返回值。
由于每次 promise.then
调用都会返回一个新创建的promise
对象,因此需要像上述方式2
那样,采用promise chain
的方式将调用进行链式化,修改后的代码如下所示。
then 返回返回新创建的promise对象
function anAsyncCall() {
var promise = Promise.resolve();
return promise.then(function() {
// 任意处理
return newVar;
});
}
Promise.all
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
使用例子
function getURL(URL) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function() {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function() {
reject(new Error(req.statusText));
};
req.send();
});
}
Promise.all([
getURL("http://www.huanghanlian.com/data_location/list.json"),
getURL("http://www.huanghanlian.com/data_location/list.json"),
getURL("http://www.huanghanlian.com/data_location/list.json"),
])
.then(function(res) {
console.log(res)
}).catch(function(error) {
console.log(error)
});
上面代码中,promises
是包含 三个 个 Promise 实例的数组,只有这 3 个实例的状态都变成fulfilled
,或者其中有一个变为rejected
,才会调用Promise.all
方法后面的回调函数。
注意,如果作为参数的 Promise
实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。
Promise.race
Promise.race
方法同样是将多个 Promise
实例,包装成一个新的 Promise
实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
Promise.all
在接收到的所有的对象promise
都变为 FulFilled
或者 Rejected
状态之后才会继续进行后面的处理, 与之相对的是 Promise.race
只要有一个promise
对象进入FulFilled
或者 Rejected
状态的话,就会继续进行后面的处理。
像Promise.all
时的例子一样,来看一个带计时器的 Promise.race
的使用例子。
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(delay);
}, delay);
});
}
// 任何一个promise变为resolve或reject 的话程序就停止运行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
])
.then(function(value) {
console.log(value); // => 1
});
创建了4个promise对象,这些promise对象会分别在1ms,32ms,64ms和128ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后, .then 注册的回调函数就会被调用,这时候确定状态的promise对象会调用 resolve(1) 因此传递给 value 的值也是1,控制台上会打印出 1 来。
再来看看在第一个promise对象变为确定(FulFilled)状态后,它之后的promise对象是否还在继续运行。
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('执行者函数是否全部执行',delay)
resolve(delay);
}, delay);
});
}
// 任何一个promise变为resolve或reject 的话程序就停止运行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
])
.then(function(value) {
console.log(value); // => 1
});
在执行者函数中增加了 console.log 用来输出调试信息。
可以看出, setTimeout 方法都会执行。
Promise.race 在第一个promise对象变为Fulfilled之后,并不会取消其他promise对象的执行。
看看在第一个结果为(Rejected)状态后,它之后的promise对象是否还在继续运行。
const p = Promise.race([
new Promise(function(resolve) {
setTimeout(function() {
//超时的请求是否会执行
console.log(1)
resolve(1);
}, 1000);
}),
new Promise(function(resolve) {
setTimeout(function() {
//超时的请求是否会执行
console.log(2)
resolve(2);
}, 2000);
}),
new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 500)
})
]);
p
.then(res => {
console.log(res)
})
.catch(error => {
console.error(error)
});
也是一样会去执行。
Promise实现原理
待续
基于对Promise用法不太熟悉的基础上,整理出以上内容,有觉得不太正确的地方的可以一起交流~
参考资料
JavaScript Promise迷你书(中文版)