Promise
是什么?为什么要使用?
为什么使用Promise
这篇关于promise
的blog其实已经是3年前写的了,但是一直在草稿状态。因为当时的项目开始使用ES6,我第一次接触到promise
这个概念,当时还花了一点时间去理解。
现在每一个前端工作者肯定非常熟悉promise
,它是用于处理异步的!那么,为什么要用promise
呢?
首先看一个项目上的例子:
let submit = function(params){
validate(params, res=>{
if(res.data === "TRUE"){
submitData(params, res=>{
if(res.data === "TRUE"){
// other actions
}
})
}
})
}
以上例子,实现一个表单提交功能,在真正把数据提交到后台之前,先要做一次校验,校验通过才允许用户提交。
再来看一下:
// 以下三个函数模拟异步方法
function job1(fn){
setTimeout(() => { fn("job1 success"); }, 150);
}
function job2(fn){
setTimeout(() => { fn("job2 success"); }, 200);
}
function job3(fn){
setTimeout(() => { fn("job3 success!"); }, 100);
}
(function(){
job1((res=>{ console.log(res); }));
job2((res=>{ console.log(res); }));
job3((res=>{ console.log(res); }));
})();
以上输出:
job3 success
job1 success
job2 success
如果我们的需求是,job1, job2, job3必须按顺序执行,代码得改成:
(function(){
job1((res=>{
console.log(res);
job2((res=>{
console.log(res);
job3((res=>{
console.log(res);
}));
}));
}));
})();
这里和上面的例子,都使用了嵌套的写法,如果逻辑再复杂一点,嵌套层数会更多,容易陷入回调地狱(callback hell)
。
Ajax
和Node.js
的回调地狱
例子就非常经典了。而promise
就是为了解决这个问题。
promise
是如何处理的呢?
如果可以写成 job1.then(job2).then(job3)... 是不是好多了?
把异步方法修改为Promise
function job1(){
return new Promise((resolve, reject)=>{
setTimeout(() => {
console.log("job1 success");
resolve("job1 success");
}, 150);
})
}
function job2(){
return new Promise((resolve, reject)=>{
setTimeout(() => {
console.log("job2 success");
resolve("job2 success");
}, 200);
})
}
function job3(){
return new Promise((resolve, reject)=>{
setTimeout(() => {
console.log("job3 success");
resolve("job3 success");
}, 100);
})
}
这时候就可以使用链式方法调用了
(function(){
job1().then(job2).then(job3).then(res=>{console.log(res);})
})();
那么,一开始的例子也可以改写成
let submit = function(params){
validate(params)
.then(submitData(params))
.then(res=>{ });
}
下面,我们一起来看看Promise是怎样实现的
什么是Promise
定义
Promise
对象用于表示一个异步操作的最终完成 (或失败)及其结果值。
状态
一个 Promise 必然处于这几种状态之一:
pending
(进行中)
fulfilled
(已成功)
rejected
(已失败)
状态的变化只有两种方法:pending
变成fulfilled
,pending
变成rejected
,状态变化时,有以下的方法来处理:
方法
then(onFulfilled, onRejected)
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve
catch(onRejected)
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise
finally(onFinally)
添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用,无论当前promise的状态是完成(fulfilled)还是失败(rejected)
// MDN上的例子
const myPromise =
(new Promise(myExecutorFunc))
.then(onFulfilledA,onRejectedA)
.then(onFulfilledB,onRejectedB)
.then(onFulfilledC,onRejectedC);
或者使用以下写法
const myPromise =
(new Promise(myExecutorFunc))
.then(onFulfilledA)
.then(onFulfilledB)
.then(onFulfilledC)
.catch(onRejectedAny);
上面的例子,就可以写成:
let onFulfilled = (data)=>{ console.log("Fulfilled: ", data); }
let onRejected = (error)=>{ console.log("Error: ", error); }
let onFinally = ()=>{ console.log("Finally."); }
(function(){
job1().then(job2).then(job3).then(onFulfilled)
.catch(onRejected)
.finally(onFinally);
})();
输出:
job1 success
job2 success
job3 success
Fulfilled: job3 success
Finally.
假如其中一个job有error,那么输出是
job1 success
job2 error
Error: job2 error
Finally.
可以看出,无论当前promise的状态是完成(fulfilled)
还是失败(rejected)
,finally()
都会被调用。
再来看看另一种写法:
(function(){
job1()
.then(job2)
.then(job3)
.then(onFulfilled,onRejected)
.finally(onFinally);
})();
使用then(onFulfilled,onRejected)
代替catch(onRejected)
,输出和以上例子一样,所以,catch(onRejected)
其实是把then(onFulfilled,onRejected)
的预留参数onFulfilled
省略了,没有本质上的区别。
再来做一点修改
(function(){
job1()
.then(job2,onRejected)
.then(job3,onRejected)
.then(onFulfilled,onRejected)
.finally(onFinally);
})();
输出:
job1 success (第二行 job1 的输出)
job2 error (第三行 job2的输出)
Error: job2 error (第四行 onRejected 的输出)
Fulfilled: undefined (第五行 onFulfilled 的输出)
Finally. (第六行 onFinally 的输出)
job2的promise
调用了reject
方法,状态变成rejected
,所以在then()
的时候调用了onRejected,但是promise
的方法都会返回一个新的promise
,所以在第五行的时候,then()
对应的promise
是上一行onRejected()
返回的promise
, 会调用onFulfilled()
任何不是 throw 的终止都会创建一个"已决议(resolved)"状态,而以 throw 终止则会创建一个"已拒绝"状态。
如果我们把onRejected()修改一下
let onRejected = (error)=>{
console.log("Error: ", error);
throw new Error(error);
}
那么,上面的输出就变成:
job1 success (第二行 job1 的输出)
job2 error (第三行 job2的输出)
Error: job2 error (第四行 onRejected 的输出)
Error: Error: job2 error (第五行 onRejected 的输出) *
at onRejected (.../test.js:34:9)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
Finally. (第六行 onFinally 的输出)*
静态方法
有一个使用得比较多的方法是Promise.all()
,先来看代码
(function() {
let p1 = job1();
let p2 = job2();
let p3 = job3();
Promise.all([p1, p2, p3]).then(values=>{
console.log(values); //
})
})();
输出:
job3 success
job1 success
job2 success
[ 'job1 success', 'job2 success', 'job3 success' ]
Promise.all()
方法接收一个promise
的iterable
类型(注:Array
,Map
,Set
都属于ES6的iterable
类型)的输入,并且只返回一个Promise
实例, 那个输入的所有promise的resolve回调的结果是一个数组。
但是这里注意一下,和上面的对比,job1、job2、job3不是按顺序执行的。
我们是不是还可能用上面then(onFulfilled,onRejected)
或者catch(onRejected)
来使用呢?
(function() {
let p1 = job1();
let p2 = job2();
let p3 = job3();
Promise.all([p1, p2, p3]).then(onFulfilled, onRejected).finally(onFinally);
})();
// 或者
(function() {
let p1 = job1();
let p2 = job2();
let p3 = job3();
Promise.all([p1, p2, p3]).then(onFulfilled).catch(onRejected).finally(onFinally);
})();
输入都是:
Error: job2 error
Finally.
Promise.all 在任意一个传入的 promise 失败时返回失败。
因为job2的状态是失败了,所以最后调用的是onRejected
Promise与事件循环
当涉及异步事件的时候,事件循环就成是了个很让人头大的问题。先来看看概念:
- 宏任务
- 主代码块
- setTimeout
- setInterval
- setImmediate ()-Node
- requestAnimationFrame ()-浏览器
- 微任务
- process.nextTick ()-Node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
为了更好了看出执行顺序,我们先来修改一下上面的job的定义
function job1(){
return new Promise((resolve, reject)=>{
console.log("job1 start...")
setTimeout(() => {
console.log("job1 success");
resolve(1);
}, 150); //定时器,150ms后执行
})
}
function job2(){
return new Promise((resolve, reject)=>{
console.log("job2 start...")
setTimeout(() => {
console.log("job2 success");
resolve(2);
}, 100); //定时器,100ms后执行
})
}
function job3(){
return new Promise((resolve, reject)=>{
console.log("job3 start...")
setTimeout(() => {
console.log("job3 success");
resolve(3);
},0);
})
}
调用方法如下
console.log("***** START ******");
let p1 = job1();
let p2 = p1.then(job2);
let p3 = p2.then(job3);
let p = p3.then(onFulfilled);
console.log(p1, p2, p3, p);
setTimeout(() => {
console.log('500ms: the stack is now empty');
console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
console.log('0ms...');
},0);
setTimeout(() => {
console.log('250ms...');
},250);
console.log("***** END ******");
输入顺序会是怎样呢?
分析:
根据事件循环,
- 先执行同步方法
console.log("***** START ******");
- 构造函数
new Promise()
是同步任务,所以执行 job1的console.log("job1 start...")
- 遇到
setTimeout
,移交给定时器线程
,150ms后放入宏任务队列
,到此job1结束 - 接下都是
Promise.then()
的方法,是异步微任务,放入微任务队列
- 执行
console.log(p1, p2, p3, p);
,这时,promise的状态都是pending
- 遇到
setTimeout
,移交给定时器线程
,500ms后放入宏任务队列
- 遇到
setTimeout
,移交给定时器线程
,0ms后放入宏任务队列
(即使是0,但是仍然要按规矩) - 遇到
setTimeout
,移交给定时器线程
,250ms后放入宏任务队列
- 执行
console.log("***** END ******")
,到这里主线程执行完毕 - 开始执行任务队列,
宏任务队列
中根据时间顺序: [0ms, 200ms,250ms, 500ms]
a. 执行console.log('0ms...');
b. 执行console.log("job1 success");
和resolve(1);
c. 执行console.log('250ms...');
d. 执行console.log('500ms: the stack is now empty'');
和console.log(p1, p2, p3, p);
但是这里注意一下,当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完 。b任务执行完的时候,p1.then(job2)
会执行,即会执行console.log("job2 start...")
,但是由于job2中也有setTimeout
,根据时间放入宏任务队列
最后输出:
***** START ******
job1 start...
Promise { } Promise { } Promise { } Promise { }
***** END ******
0ms...
job1 success
job2 start...
job2 success
job3 start...
250ms...
job3 success
Fulfilled: 3
500ms: the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { 'Completed!' }
最后所有promise都是fulfilled/rejected状态
Promise.all()的同步和异步
如果使用Promise.all()
呢?
console.log("***** START ******");
let p1 = job1();
let p3 = job3();
let p2 = job2();
let p = Promise.all([p1, p2, p3]);
let ep = Promise.all([]);
console.log(p1, p2, p3);
console.log(ep, p);
setTimeout(() => {
console.log('the stack is now empty');
console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
console.log('0ms...');
},0);
console.log("***** END ******")
结果:
***** START ******
job1 start...
job3 start...
job2 start...
Promise { } Promise { } Promise { }
Promise { [] } Promise { }
***** END ******
job3 success
0ms...
job2 success
job1 success
the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { [ 1, 2, 3 ] }
这里有一个注意点:
Promise.all
当且仅当传入的可迭代对象为空时为同步
所以最开始的时候,console.log(ep, p);
的输出一个是fulfilled
,一个是pending
async/await
最后顺便看看 ES2017新增的 async/await
吧
await
关键字接收一个promise
并奖其转换为一个返回值或抛出一个异常
async
关键字意味着函数返回一个promise
任何使用
await
的代码都是异步的,只能在async
关键字声明的函数内部使用await
关键字
上面的例子,如果想要取出每一步的结果,可能会比较麻烦,可以改写成
async function run() {
// 按顺序执行
let r1 = await job1();
let r2 = await job2();
let r3 = await job3();
console.log(r1,r2, r3);
}
// output: 1 2 3
或使用Promise.all
async function run() {
// 不会按顺序执行
let [r1,r2, r3] = await Promise.all([job1(), job2(), job3()]);
console.log(r1,r2, r3);
}
// output: 1 2 3
参考文章:
HTML Standard
MDN上的说明
Promise+
讲JS运行机制,事件循环讲得很清晰