浅谈 Promise 对象
一、介绍
说到 Promise 相信很多前端工作者都不会陌生,但是很多人用它,却对其原理模糊不清,在深度学习了 Promise 原理后,决定浅谈一下这个神秘对象内部是如何实现的? 又解决了哪些问题?
Promise 规范有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升级版 Promise/A+,而 ES6 中采用了 Promise/A+ 规范。说明对这套规范的认可度很高,本文也将围绕这套规范展开详说。
二、初识 Promise
Promise 的诞生是为了解决异步编程问题,最早由社区提出并实现,ES6 将其写进了语言标准,统一了用法,随之由原生推出了 Promise 对象。
阮神的ES6入门中说:Promise,简单来讲就是一个容器,里面保存着某种未来才会结束的事件,白话就是:一个处理异步操作的结果的容器;从语法层面来讲,Promise 是一个对象,它可以获取异步操作的消息。Promise 提供统一的API,各种异步操作都可以用同样的方法进行处理。
三、Promise 对象的两个特点
1.状态改变不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Fulfilled(已成功)和Rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态。
2.状态一旦改变就不会再变,任何时候都可以得到这个状态的结果。而状态也只可能是从 Pending 变为 Fulfilled 或者从 Pending 变为 Rejected,之后的状态不会再改变,这个状态称为 Resolved(已定型)
四、Promise 使用场景
为了更接近开发中使用的场景,我们从一段代码开始分析 Promise 的作用。
//不使用Promise
http.get('url', function (result) {
//do something
console.log(result.id);
});
//使用Promise
new Promise(function (resolve) {
//异步请求
http.get('url', function (result) {
resolve(result.id)
})
}).then(function (id) {
//do something
console.log(id);
})
对比来看似乎不使用 Promise 代码更简洁易懂,但是场景稍微复杂,我们就能看出一个很明显的问题,那就是回调地狱。
//不使用Promise
http.get('url', function (id) {
//do something
http.get('getNameById', id, function (name) {
//do something
http.get('getCourseByName', name, function (course) {
//dong something
http.get('getDetailByCourse', function (Detail) {
//do something
})
})
})
});
//使用Promise
function getUserId(url) {
return new Promise(function (resolve) {
//异步请求
http.get(url, function (id) {
resolve(id)
})
})
}
getUserId('some_url').then(function (id) {
//do something
return getNameById(id); // getNameById 是和 getUserId 一样的Promise封装。下同
}).then(function (name) {
//do something
return getCourseByName(name);
}).then(function (course) {
//do something
return getDetailByCourse(course);
}).then(function (courseDetail) {
//do something
});
其实细心的小伙伴已经发现了,Promise 也还是使用回调函数,只不过是把回调封装在了内部,使用上一直通过 then 方法的链式调用,使得多层的回调嵌套看起来变成了同一层的,书写上以及理解上会更直观和简洁一些。没有多层嵌套,变得利于阅读了一点。
来看一个基础版本:
//极简的实现
class Promise {
callbacks = [];
constructor(fn) {
fn(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
}
_resolve(value) {
this.callbacks.forEach(fn => fn(value));
}
}
//Promise应用
let p = new Promise(resolve => {
setTimeout(() => {
console.log('done');
resolve('5秒');
}, 5000);
}).then((tip) => {
console.log(tip);
})
这个版本的逻辑就是:
调用 then 方法,将想要在 Promise 异步操作成功时执行的 onFulfilled 放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;
创建 Promise 实例时传入的函数会被赋予一个函数类型的参数,即 resolve,它接收一个参数 value,代表异步操作返回的结果,当异步操作执行成功后,会调用resolve方法,这时候其实真正执行的操作是将 callbacks 队列中的回调一一执行;
首先 new Promise 时,传给 Promise 的函数设置定时器模拟异步的场景,接着调用 Promise 对象的 then 方法注册异步操作完成后的 onFulfilled,最后当异步操作完成时,调用 resolve(value), 执行 then 方法注册的 onFulfilled。
then 方法注册的 onFulfilled 是存在一个数组中,可见 then 方法可以调用多次,注册的多个onFulfilled 会在异步操作完成后根据添加的顺序依次执行。如下:
//then 的用法
let p = new Promise(resolve => {
setTimeout(() => {
console.log('done');
resolve('2s');
}, 2000);
});
p.then(res => {
console.log('then1', res);
});
p.then(res => {
console.log('then2', res);
});
上例中,要先定义一个变量 p ,然后 p.then 两次。而规范中要求,then 方法应该能够链式调用。实现也简单,只需要在 then 中 return this 即可。如下所示:
//简单的实现+链式调用
class Promise {
callbacks = [];
constructor(fn) {
fn(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
return this; // Look
}
_resolve(value) {
this.callbacks.forEach(fn => fn(value));
}
}
let p = new Promise(resolve => {
setTimeout(() => {
console.log('done');
resolve('2s');
}, 2000);
});
p.then(res => {
console.log('then1', res);
});
p.then(res => {
console.log('then2', res);
});
五、异常处理
异常通常是指在执行成功/失败回调时代码出错产生的错误,对于这类异常,我们使用 try-catch 来捕获错误,并将 Promise 设为 rejected 状态即可。
function handle(callback){
if(state === 'pending'){
callbacks.push(callback)
return;
const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected;
const next = state === 'fulfilled'? callback.resolve:callback.reject;
if(!cb){
next(value) return;
} try {
const ret = cb(value) next(ret)
} catch (e) {
callback.reject(e);
}
}
更多是习惯注册 catch 方法来处理错误,例:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ test: 1 })
}, 1000)
}).then((data) => {
console.log('result1', data)
//dosomething
return test()
}).catch((ex) => {
console.log('error', ex)
})
实际上,错误也好,异常也罢,最终都是通过reject实现的。也就是说可以通过 then 中的错误回调来处理。所以我们可以增加这样的一个 catch 方法:
function Promise(fn){
...
this.then = function (onFulfilled,onRejected){
return new Promise((resolve, reject)=>{
handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
this.catch = function (onError){ this.then(null,onError)
}
...
}
六、resolve 方法和 reject 方法
实际应用中,我们可以使用 Promise.resolve 和 Promise.reject 方法,用于将于将非 Promise 实例包装为 Promise 实例。如下例子:
Promise.resolve({name:'winty'})
Promise.reject({name:'winty'})
// 等价于
new Promise(resolve => resolve({name:'winty'}))
new Promise((resolve,reject) => reject({name:'winty'}))
这些情况下,Promise.resolve 的入参可能有以下几种情况:
- 无参数 [直接返回一个resolved状态的 Promise 对象]
- 普通数据对象 [直接返回一个resolved状态的 Promise 对象]
- 一个Promise实例 [直接返回当前实例]
- 一个thenable对象(thenable对象指的是具有then方法的对象) [转为 Promise 对象,并立即执行thenable对象的then方法。]
基于以上几点,我们可以实现一个 Promise.resolve 方法如下:
function Promise(fn){
...
this.resolve = function (value){
if (value && value instanceof Promise) {
return value;
} else if (value && typeof value === 'object' && typeof value.then === 'function'){
let then = value.then;
return new Promise(resolve => {
then(resolve);
});
} else if (value) {
return new Promise(resolve => resolve(value));
} else {
return new Promise(resolve => resolve());
}
}
...
}
Promise.reject与Promise.resolve类似,区别在于Promise.reject始终返回一个状态的rejected的Promise实例,而Promise.resolve的参数如果是一个Promise实例的话,返回的是参数对应的Promise实例,所以状态不一 定。
因此,reject 的实现就简单多了,如下:
function Promise(fn){
...
this.reject = function (value){
return new Promise(function(resolve, reject) {
reject(value);
});
}
...
}
入参是一个 Promise 的实例数组,然后注册一个 then 方法,然后是数组中的 Promise 实例的状态都转为 fulfilled 之后则执行 then 方法。这里主要就是一个计数逻辑,每当一个 Promise 的状态变为 fulfilled 之后就保存该实例返回的数据,然后将计数减一,当计数器变为 0 时,代表数组中所有 Promise 实例都执行完毕。
function Promise(fn){
...
this.all = function (arr){
var args = Array.prototype.slice.call(arr);
return new Promise(function(resolve, reject) {
if(args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
try {
if(val && (typeof val === 'object' || typeof val === 'function')) {
var then = val.then;
if(typeof then === 'function') {
then.call(val, function(val) {
res(i, val);
}, reject);
return;
}
}
args[i] = val;
if(--remaining === 0) {
resolve(args);
}
} catch(ex) {
reject(ex);
}
}
for(var i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
}
...
}
七、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决定,分成两种情况。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
let promise = Promise.all([1, 2, 3]);
promise.then(value => {
console.log(value); // [1,2,3]
});
console.log(promise);
情景一:promise结果
let p2 = Promise.reject(2);
let promise = Promise.all([1, p2, 3]);
promise
.then(value => {
console.log(value);
})
.catch(err => {
console.log(err); // 2
});
console.log(promise);
情景二:promise结果
如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法
const p1 = new Promise(resolve => {
resolve("hello");
})
.then(result => result)
.catch(e => e);
const p2 = new Promise(() => {
throw new Error("报错了");
})
.then(result => result)
.catch(e => e); // p2实际上是catch返回的promise实例
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
Promise.race()
总结
看似复杂的Promise背后其实推敲也能分析出其中的原理,分析每一个步的执行过程,想一下它存在的作用,解决什么问题,用于什么场景,最关键的点就是要理解 then 函数 ,它负责注册回调,真正的执行是在Promise的状态被改变之后,而当reslove的入参是一个Promise时,想要链式调用起来,就必须调用其 then 方法 then.call 将上一个Promise方法注入其回调数组中。