深入理解 Promise(上)
为了解决回调函数带来的问题,Promise 作为一种更优雅的异步解决方案被提出,最初只是一种实现接口规范,而到了 es6,则是在语言层面就原生支持了 Promise 对象。
所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 也有一些缺点。
- 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
如果某些事件不断地反复发生,一般来说,使用 stream 模式是比部署 Promise 更好的选择。
一、基本用法
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。
这两个函数都接受Promise对象传出的值作为参数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
二、相关 API
1. Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
p.then((val) => console.log("fulfilled:", val))
.catch((err) => console.log("rejected:", err));
// 等同于
p.then((val) => console.log("fulfilled:", val))
.then(null, (err) => console.log("rejected:", err));
一般来说,不要在then方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch方法,而不使用then方法的第二个参数。
跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。
2. Promise.all()
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。)
p的状态由p1、p2、p3决定,分成两种情况。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON("/post/" + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
上面代码中,promises是包含6个Promise实例的数组,只有这6个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
3. Promise.race()
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.race([p1,p2,p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。
Promise.race方法的参数与Promise.all方法一样,如果不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将Promise的状态变为reject,否则变为resolve。
相当于,多个promise值进行比赛,看谁的状态最先改变,就将谁的值传递给回调函数。
下面基于promise.race([p1,p2,p3])实现超时处理:
function delayPromise(wait) {
return new Promise((resolve) => {
setTimeout(resolve, wait);
});
}
function timeoutPromise(promise, wait) {
let timeout = delayPromise(wait).then(() => {
throw new Errow(`超过${wait}ms未完成`);
});
return Promise.race([promise, timeout]);
}
// 测试
var promise = new Promise((resolve, reject) => {
let random = Math.random()*1000
//setTimeout(resolve(random),random) ???????立即执行了
setTimeout(() => {
resolve(random)
}, random)
})
timeoutPromise(promise, 300)
.then((value)=> {
console.log(`规定${value}ms时间内完成`)
})
.catch((err) => {
console.log(`发生超时:${err}`)
})
4. Promise.resolve()
有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代码将jQuery生成的deferred对象,转为一个新的Promise对象。
Promise.resolve等价于下面的写法。
Promise.resolve('foo');
// 等价于
new Promise(resolve => resolve('foo'));
Promise.resolve方法的参数分成四种情况。
- 参数是一个Promise实例
如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
- 参数是一个thenable对象
thenable对象指的是具有then方法的对象,比如下面这个对象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。
上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出42。
- 参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved。
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代码生成一个新的Promise对象的实例p。由于字符串Hello不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise实例的状态从一生成就是Resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。
- 不带有任何参数
Promise.resolve方法允许调用时不带参数,直接返回一个Resolved状态的Promise对象。
所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。
var p = Promise.resolve();
p.then(function () {
// ...
});
上面代码的变量p就是一个Promise对象。
5. Promise.reject()
Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。它的参数用法与Promise.resolve方法完全一致。
var p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s){
console.log(s)
});
// 出错了
上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。
参考文章
通过深入理解 Promise 内部原理,下面动手实现一个 Promise() 方法
function myPromise(fn) {
this.value = null;
this.status = 'pending';
this.resolveFun = function(){}; // 成功回调的方法就得变成数组才能存储 最好写成 this.resolveFun = []
this.rejectedFun = function(){};
fn(this.resolve.bind(this));
}
myPromise.prototype.resolve = function(val) {
//let _this = this;
if(this.status == 'pending') {
this.status = 'fullfilled';
this.value = val;
setTimeout(() => {
this.resolveFun(this.value);
}, 0);
}
};
// .then() 每次返回一个新的 Promise 对象
myPromise.prototype.then = function(fullfilled) {
fullfilled = typeof fullfilled === 'function' ? fullfilled : (value) => {return value}; // 实现穿透
//let _this = this;
return new myPromise((resolve, reject) => {
this.resolveFun = () => {
let res = fullfilled(this.value);
if(res && typeof res.then == 'function') {
res.then(resolve, reject);
} else {
resolve(res);
}
}
});
};
完整版:
class myPromise {
constructor(fn) {
this.value = null;
this.status = 'pending';
this.resolveFunc = function(){}; // 异步操作成功后的回调
this.rejectedFunc = function(){};
fn(this.resolve.bind(this), this.reject.bind(this)); // fn() 中执行异步操作
}
/*
调用 then() 注册回调函数, 类似观察者模式
*/
then(onFulfilled, onRejected) {
fullfilled = typeof fullfilled === 'function' ? fullfilled : (value) => {return value};
onRejected = typeof onRejected === 'function' ? onRejected : (value) => {return value};
// this.resolveFunc = onFulfilled;
// this.rejectedFunc = onRejected;
return new myPromise((resolve, reject) => {
// if(this.status === 'pending') { // 不能加这个判断 无论什么时候都会执行 下面的 this.resolveFunc 不起作用
// this.resolveFunc = onFulfilled;
// this.rejectedFunc = onRejected;
// return;
// }
// 如果 then() 中没有传递任何参数
if(!onFulfilled && !onRejected) {
resolve(this.value); // 要把value resolve 出去,因为可能后续还有.then()操作,不能断链,能继续形成链式调用
}
this.resolveFunc = () => { // 改变外部 Promise 的回调
let res = onFulfilled(this.value); // 执行传进 .then() 的参数,判断是否返回一个Promise对象
if(res && typeof res.then === 'function') {
res.then(resolve, reject); // ???
} else {
resolve(res);
}
};
this.rejectedFunc = () => {
let res = onRejected(this.value);
if(res && typeof res.then === 'function') {
res.then(resolve, reject);
} else {
reject(res);
}
}
});
}
/*
val 代表一步操作返回的结果
当异步操作执行成功后,使用 resolve() ,开始一一执行 resolveFunc 中的回调
并通过 setTimeout 机制,保证在执行 resolve 之前,then 方法已经完成回调函数的注册
*/
resolve(val) {
if(this.status === 'pending') {
this.status = 'fullfilled';
this.value = val;
setTimeout(() => {
this.resolveFunc(this.value);
}, 0);
}
}
reject(val) {
if(this.status === 'pending') {
this.status = 'rejected';
this.value = val;
setTimeout(() => {
this.rejectedFunc(this.value);
}, 0);
}
}
}
测试:
- 回到过去: resolve, reject和then
// 测试
var fn=function(resolve, reject){
console.log('begin to execute!');
var number=Math.random();
if(number<=0.5){
resolve('less than 0.5');
}else{
reject('greater than 0.5');
}
}
var p = new myPromise(fn);
p.then(function(data) {
console.log('resolve: ', data);
}, function(data) {
console.log('reject: ', data);
});
- 加入状态: pending, fullfilled, rejected
// 测试
var fn=function(resolve, reject){
resolve('hello');
reject('hello again');
}
var p1=new Promise(fn);
p1.then(function(data){
console.log('resolve: ',data)
}, function(data){
console.log('reject: ',data)
});
//'resolve: hello'
var p2=new myPromise(fn);
p2.then(function(data){
console.log('resolve: ',data)
}, function(data){
console.log('reject: ',data)
});
p1是原生Promise,p2是我们自己写的,可以看出,当调用resolve之后再调用reject,p1只会执行resolve,如果不加入状态我们的则是两个都执行。
事实上在Promise规范当中,规定Promise只能从初始pending状态变到resolved或者rejected状态,是单向变化的,也就是说_执行了resolve就不会再执行reject_,反之亦然。
为此,我们需要在MyPromise中加入状态,并在必要的地方进行判断,防止重复执行:
- 链式调用
在Promise的使用中,我们一定注意到,是可以链式调用的:
要实现链式调用,then方法的返回值也必须是一个Promise对象,这样才能再次在后面调用then。
观察Promise规范,会发现链式调用的情况也分两种。一种情况下,前一个Promise的resolve或者reject的返回值是普通的对象;但还有一种情况,就是前一个Promise的resolve或者reject执行后,返回的值本身又是一个Promise对象。
因此我们需要对回调函数的执行结果做一次判断,看其是否是一个 Promise 对象。通过调用 .then 看它有没有 then() 方法来判断。 typeof res.then === 'function'
var fn=function(resolve, reject){
resolve('hello');
}
var p1=new Promise(fn);
p1.then(function(data){
console.log(data);
return 'hello again';
}).then(function(data){
console.log(data);
});
//'hello'
//'hello again'
var fn=function(resolve, reject){
resolve('hello');
}
var p1=new Promise(fn);
p1.then(function(data){
console.log(data);
return 'hello again';
}).then(function(data){
console.log(data);
return new Promise(function(resolve){
var innerData='hello third time!';
resolve(innerData);
})
}).then(function(data){
console.log(data);
});
这里一定要注意的点是:promise里面的 then 函数仅仅是注册了后续需要执行的代码(只是注册了异步操作结束后的回调函数),回调函数的真正的执行是在 resolve 方法里面执行的。
- 链式调用为什么要返回新的 promise
如我们理解,为保证 then 函数链式调用,then 需要返回 promise 实例。但为什么返回新的 promise,而不直接返回 this 当前对象呢?看下面示例代码:
var promise2 = promise1.then(function (value) {
return Promise.reject(3)
})
假如 then 函数执行返回 this 调用对象本身,那么 promise2 === promise1,promise2 状态也应该等于 promise1 同为 resolved。而 onResolved 回调中返回状态为 rejected 对象。考虑到 Promise 状态一旦 resolved 或 rejected就不能再迁移,所以这里 promise2 也没办法转为回调函数返回的 rejected 状态,产生矛盾。
- Promise值的穿透
new Promise(resolve=>resolve(8))
.then()
.catch()
.then(function(value) {
alert(value)
})
即相当于:
new Promise(resolve=>resolve(8))
.then(function(value){
return value
})
.catch(function(reason){
throw reason
})
.then(function(value) {
alert(value)
})
所以如果想要把then的实参留空且让值可以穿透到后面,意味着then的两个参数的默认值分别为function(value) {return value},function(reason) {throw reason}。
所以我们只需要把then里判断onResolved和onRejected的部分改成如下即可:fullfilled = typeof fullfilled === 'function' ? fullfilled : (value) => {return value};
onRejected = typeof onRejected === 'function' ? onRejected : (value) => {throw value};
一旦promise resolve一个值,他就应该一直保留那个值,并且永远也不会再resolve
最后,看一道2019年阿里校招的一道在线编程题
const timeout = ms => new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
const ajax1 = () => timeout(2000).then(() => {
console.log('1');
return 1;
});
const ajax2 = () => timeout(1000).then(() => {
console.log('2');
return 2;
});
const ajax3 = () => timeout(2000).then(() => {
console.log('3');
return 3;
});
const mergePromise = ajaxArray => {
// your code
};
console.time('sequence')
mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log('done');
console.log(data); // data 为 [1, 2, 3]
console.timeEnd('sequence')
});
// 分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
答案:
const mergePromise = ajaxArray => {
let result=[];
return ajaxArray.reduce((res, cur) => {
return res
.then(cur)
.then(item => {
result.push(item);
return result;
})
}, Promise.resolve())
};
看一下,使用 Promise.all()
的结果: 虽然结果是顺序输出,但是执行顺序不是顺序执行的
console.time('serial');
Promise.all([ajax1(), ajax2(), ajax3()]).then(data=>{
console.log('done');
console.log(data);
console.timeEnd('serial')
})
// 分别输出
// 2
// 1
// 3
// done
// [1, 2, 3]
References
30分钟,让你彻底明白Promise原理
彻底理解Promise对象——用es5语法实现一个自己的Promise(上篇)
解读Promise内部实现原理
剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 浏览器跨域解决方案
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论