前言
Promise是当前ES6语法中的异步解决方案,本文从基本概念开始,层层分析,一步一步手写实现Promise,希望能和大家一起彻底掌握Promise。
概述
Promise是异步编程的一种解决方案,跟传统回调函数来实现异步相比,其设计更合理,代码更易懂。Promise是”美颜后”的异步解决方案.
在ES6中, 其写进了语言标准提供原生支持。
Promise有两大特点:
- 对象的状态不受外界的影响,Promise对象代表一个异步操作,有三种状态:
pending
,fulfilled
,rejected
- Promise是一个状态机,一旦状态改变,就不会再变,并且在任何时候都可以得到这个结果。
上图摘自MDN
Promise的具体用法可以参考阮一峰老师的 《ECMAScript 6 入门》或MDN。
这里有几个点需要注意:
- Promise可以有finally,用于做跟状态无关的业务逻辑操作。
Promise中定义的函数在resolve之后还是可以执行语句,所以建议在resolve前加上return
new Promise((resolve, reject) => {
resolve(1);//为了防止输出2,改成return resolve,日常编码中也要养成return resolve的习惯
console.log(2);// 可以输出2
}).then(r => {
console.log(r);
});
then方法中的第二个参数
reject=>{}
可以省略,建议使用catch(error=>{})
,其不仅能够获取reject的内容,还可以捕获到Promise函数和then函数中产生的异常。Promise内部的异常会被吃掉,不会被外部的异常捕获。- 注意Promise.all和Promise.race的用法及使用场景。
如果熟悉了Promise的使用,其实我们知道,Promise提供了异步编程的语法糖,使原来异步回调的操作可以用同步的方式来表达。
回调地狱
在传统AJAX异步解决方案中,我们一般使用回调函数来解决数据的接收和处理,如下:
$.get(url, (data) => {
console.log(data)
)
在某些需求场景下,我们需要发送多个异步请求,并且每个请求之间结果之间需要相互依赖,随着回调函数相互嵌套的增加,函数之间的逻辑就会耦合在一起,难以维护,形成回调地狱。如下所示:
let country = 'china';
let city = 'shanghai';
let district = 'PD'
$.get(`xxxxx/countries`,countries=>{
/**
**这里可以再第一个select控件中,渲染国家列表,
**/
countries.forEach((item)=>{
if(item===country){
//查找并选中当前国家
$.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
/**
**这里可以再第二个select控件中,渲染城市列表,
**/
cities.forEach((item)=>{
if(item === city){
//查找并选中当前城市
$.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
/**
**这里可以再第三个select控件中,渲染地区列表,
**/
dists.forEach(item=>{
if(item==district){
//查找并选中地区
}
})
})
}
})
})
}
});
});
上述是一个简单的三级联动功能,使用三个回调函数。它们相互嵌套逻辑复杂,耦合严重。
Promise解决了回调地狱问题,通过Promise将上述代码改写成
let country = 'china';
let city = 'shanghai';
let district = 'PD'
new Promise(() => {
$.get(`xxxxx/countries`, countries => {
return countries;
});
}).then((countries) => {
countries.forEach((item) => {
if (item === country) {
$.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
return cities;
})
}
})
}).then((cities) => {
cities.forEach((item) => {
if (item === city) {
$.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
return dists;
})
}
})
}).then((dists) => {
dists.forEach(item => {
if (item == district) {
//查找并选中地区
}
})
})
此时,将异步执行由原来的回调,改成了 then...then....then...
链式调用的方式。
线性的链式执行(同步)更符合人类的思考习惯(更直白的说,按照顺序一步一步的闭环符合人类思考习惯。Promise就是将原本异步回调的语法形式,改写成同步。所以实现Promise,就是把异步回调这种丑陋的方式改成链式调用。通过手写Promise,我们来理解和消化其设计思想。
开始
有了上述的铺垫,我们了解Promise的概念和特征,也知道了Promise的优势,下面我们一步步来实现Promise。
Promise是一个构造函数,并且传入的参数是一个函数,并且该函数在构造函数中执行。
function Promise(executor){ try{ executor() }catch(e){ console.log(e): } }
executor函数的两个参数resolve和reject,是executor函数中的回调函数。
function Promise(executor){ function resolve(value){ //可以将executor中的数据传入resolve中 } function reject(value){ //可以将executor中的数据传入reject中 } try{ executor(resolve,reject) }catch(e){ console.log(e): } }
- Promise实现了状态机,在执行resolve时,由
PENDING=>FULFILLED
,在执行reject时,由PENDING=>REJECTED
。 ```javascript const pending = ‘PENDING’; const rejecting = ‘REJECTED’; const fulfilled = ‘FULFILLED’; function Promise(executor){ var that = this; that.status = pending; that.value = null; that.error = null; function resolve(val){ //当且仅当PENDING=》FULFILLED if(that.status === pending){ that.status = fulfilled;
} } function reject(val){ //当且仅当PENDING=》REJECTED if(that.status === pending){ that.status = rejecting;that.value = val;
} } try{ executor(resolve,reject); }catch(e){ //在executor中产生的异常在reject中可以捕获。但是reject的异常,智能catch捕获 reject(e); } }that.error = val;
Promise.prototype.then = function(onFulfilled, onRejected){ var that = this; if(that.status === fulfilled){ //当状态改变后,执行then中的回调 onFulfilled(that.value); } if(that.status === rejecting){ //同上 onRejected(that.error) } };
执行如下代码
```javascript
new Promise((resolve)=>{
resolve(1);
}).then((res)=>{
console.log(res);
});
打印结果如下
- Promise是异步的
如果executor函数存在异步,则需要等待resolve或者reject回调执行才会执行then中的函数体。
此处使用回调来解决异步:
第一步:定义两个Promise的成员:onResolvedCallBack和onRejectCallBack,存储then函数中的入参。
第二步:当执行then函数时,如果当前状态还是PENDING(此时executor内部有异步操作)那么就将then中的参数传入onResolvedCallBack和onRejectCallBack中。如果此时状态是非PENDING,那么直接执行传入的函数即可。
第三步:Promise中的resolve函数执行时,触发onResolvedCallBack或者onRejectCallBack中的函数。
具体代码如下:
const pending = 'PENDING';
const rejecting = 'REJECTED';
const fulfilled = 'FULFILLED';
function Promise1(executor) {
var that = this;
that.status = pending;
that.value = null;
that.error = null;
that.resolvedCallbacks = [];
that.rejectedCallbacks = [];
function resolve(val) {
if (that.status === pending) {
that.status = fulfilled;
that.value = val;
that.resolvedCallbacks.map(cb => cb(that.value));
}
}
function reject(val) {
if (that.status === pending) {
that.status = rejecting;
that.error = val;
that.rejectedCallbacks.map(cb => cb(that.value));
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
Promise1.prototype.then = function (onFulfilled, onRejected) {
var that = this;
//为了保证兼容性,then的参数只能是函数,如果不是要防止then的穿透问题
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected =
typeof onRejected === 'function'
? onRejected
: r => {
throw r
}
if (that.status === pending) {
that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
}
if (that.status === fulfilled) {
onFulfilled(that.value);
}
if (that.status === rejecting) {
onRejected(that.error)
}
};
执行如下一段代码:
let p = new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 3000)
});
p.then((res) => { console.log(res); });
console.log(2);
打印结果如下:
我们再来执行如下代码
let p = new Promise((resolve) => {
resolve(1);
});
p.then((res) => { console.log(res); });
console.log(2);
此处我们打印结果为
但是真正的Promise打印结果为:先2后1
- Promise的then属于microtasks
microtask和macrotask是Event Loop的知识点,关于Event Loop可以参考阮一峰老师的《JavaScript 运行机制详解:再谈Event Loop》
此处我们使用setTimeout来模拟then的microtasks(注:)
function resolve(value) {
setTimeout(() => {
if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}, 0)
}
function reject(value) {
setTimeout(() => {
if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}, 0)
}
- resolve支持传入Promise对象
我们执行如下代码:
let p = new Promise((resolve) => {
var a = new Promise((resolve) => { resolve(1) });
resolve(a);
});
p.then((res) => {
console.log(res);
});
此处resolve传入的是Promise对象,打印结果为:
所以在resolve函数中需要对value做一次判断
function resolve(value) {
if (val instanceof Promise) {
return val.then(resolve, reject);
}
setTimeout(() => {
if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}, 0)
}
- then可以链式调用
在Promise中,在then中执行return语句,返回的一定是Promise对象,这也是then能够链式调用的原因。
首先我们将then中的如下片段
if (that.status === pending) {
that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
}
变形
if (that.status === pending) {
that.resolvedCallbacks.push(()=>{onFulfilled(that.value)});
that.rejectedCallbacks.push(()=>{onRejected(that.value)});
}
它们之间只是写法的差异,效果相同。
因为我们需要对then里传入的函数onFulfilled, onRejected返回的值进行判断,所以我们需要对then继续改写
if (that.status === pending) {
that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
that.rejectedCallbacks.push(()=>{const x = onRejected(that.value)});
}
因为then返回的是Promise,所以继续完善
if (that.status === pending) {
return new Promise(resolve,reject){
that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
that.rejectedCallbacks.push(()=>{const x = onRejected(that.error)});
}
}
执行onFulfilled和onRejected时,使用try…catch…,所以继续完善
let promise2 = null;
if (that.status === pending) {
return promise2 = new Promise((resolve,reject)=>{
that.resolvedCallbacks.push(()=>{
try{
const x = onFulfilled(that.value);
}catch(e){
reject(e);
}
});
that.rejectedCallbacks.push(()=>{
try{
const x = onRejected(that.error);
}catch(e){
reject(e);
}
});
});
}
上述x是onFulfilled(that.value)和onRejected(that.error)的返回值,为了保证then可以链式调用,也就是promise2的resolve能够resolve一个Promise对象,但是x返回的可能是Promise对象,可能是值,也可能是函数,那么此处需要对x进行适配一下。此时引入resolvePromise函数,实现如下:
/**
* 对resolve 进行改造增强 针对x不同值情况 进行处理
* @param {promise} promise2 promise1.then方法返回的新的promise对象
* @param {[type]} x promise1中onFulfilled的返回值
* @param {[type]} resolve promise2的resolve方法
* @param {[type]} reject promise2的reject方法
*/
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
// 如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错
return reject(new TypeError('循环引用'));
}
let called = false; // 避免多次调用
// 如果x是一个promise对象 (该判断和下面 判断是不是thenable对象重复 所以可有可无)
if (x instanceof Promise) { // 获得它的终值 继续resolve
if (x.status === PENDING) { // 如果为等待态需等待直至 x 被执行或拒绝 并解析y值
x.then(y => {
resolvePromise(promise2, y, resolve, reject);
}, reason => {
reject(reason);
});
} else {
// 如果 x 已经处于执行态/拒绝态(值已经被解析为普通值),用相同的值执行传递下去
x.then(resolve, reject);
}
// 如果 x 为对象或者函数
} else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
try { // 是否是thenable对象(具有then方法的对象/函数)
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if(called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, reason => {
if(called) return;
called = true;
reject(reason);
})
} else { // 说明是一个普通对象/函数
resolve(x);
}
} catch(e) {
if(called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
此时that.status === pending的代码块也要继续修改
if (that.status === pending) {
return promise = new Promise1((resolve, reject) => {
that.resolvedCallbacks.push(() => {
try {
const x = onFulfilled(that.value);
resolvePromise(promise,x,resolve,reject);
} catch (e) {
reject(e);
}
});
that.rejectedCallbacks.push(() => {
try {
const x = onRejected(that.error);
} catch (e) {
reject(e);
}
});
})
}
同理that.status===fulfilled和that.status===rejecting的时候代码如下:
if (that.status === FULFILLED) { // 成功态
return promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
try{
let x = onFulfilled(that.value);
resolvePromise(promise2, x, resolve, reject);
} catch(e) {
reject(e);
}
});
})
}
if (that.status === REJECTED) {
return promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(that.reason);
resolvePromise(promise2, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}
- Promise的all和race
Promise.all的用法如下
const p = Promise.all([p1, p2, p3]).then((resolve)=>{},(reject)=>{});
Promise.all方法接受一个数组作为参数,只有当数组的所有Promise对象的状态全部fulfilled,才会执行后续的then方法。
根据all的用法和特点,我们推测Promise.all返回一个Promise对象,在Promise对象中去等待Promise数组对象的函数执行完毕,数组中的每个对象执行完毕都+1,当等于数组的长度时,resolve数组对象中所有resolve出来的值。
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let done = gen(promises.length, resolve);
promises.forEach((promise, index) => {
promise.then((value) => {
done(index, value)
//每执行一次都会往values数组中放入promise对象数组成员resolve的值
//当values的长度等于promise对象数组的长度时,resolve这个数组values
}, reject)
})
})
}
//使用闭包,count和values在函数的声明周期中都存在
function gen(length, resolve) {
let count = 0;
let values = [];
return function(i, value) {
values[i] = value;
if (++count === length) {
console.log(values);
resolve(values);
}
}
}
Promise.race的用法和all类似,区别就是promise对象数组其中有一个fulfilled,就执行then方法,实现如下
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise.then(resolve, reject);
});
});
}
需要注意的是all和race函数中的数组成员不一定是Promise对象,如果不是Promise提供了resolve方法将其转化成Promise对象。resolve的实现很简单如下:
Promise.resolve = function (value) {
return new Promise(resolve => {
resolve(value);
});
}
参考
《ES6入门-阮一峰》
《ES6 系列之我们来聊聊 Promise》
《异步解决方案——Promise与Await》
《Promise原理讲解 && 实现一个Promise对象》
《面试精选之Promise》
《Promise/A+》
《Promise之你看得懂的Promise》
《八段代码彻底掌握 Promise》
《Promise不会??看这里!!!史上最通俗易懂的Promise!!!》
《Promise 必知必会(十道题)》