一. 为什么需要 promise ?
Javascript 是一⻔单线程语言,所以早期我们解决异步的场景时,大部分情况都是通过回调函数来进
行。
例如在浏览器中发送 ajax 请求,就是常⻅的一个异步场景,发送请求后,一段时间服务端响应之后我们 才能拿到结果。如果我们希望在异步结束之后执行某个操作,就只能通过回调函数这样的方式进行操 作。
var dynamicFunc = function(cb) { setTimeout(function() {
cb();
}, 1000);
}
dynamicFunc(function() {console.log(123)});
- 上面这个例子的 dynamicFunc 就是一个异步的函数
- 里面执行的 setTimeout 会在 1s 之后调用传入的 cb 函数
- 照上面的调用方式,最终 1s 之后,会打印 123 这个结果
同样的,如果后续还有内容需要在异步函数结束时输出的话,就需要多个异步函数进行嵌套,非常不利 于后续的维护。
setTimeout(function() {
console.log(123);
setTimeout(function() {
console.log(321);
// ...
}, 2000);
}, 1000);
为了能使回调函数以更优雅的方式进行调用,在 ES6 中 js 产生了一个名为 promise 的新规范,他让异步操作的变得近乎「同步化」。
二. Promise 基础
在支持 ES6 的高级浏览器环境中,我们通过 Promise()
即可构造一个 promise 实例。
这个构造函数接受一个函数,分别接受两个参数,resolve 和 reject,代表着我们需要改变当前实例的状 态到 已完成
或是 已拒绝
。
function promise1() {
return new Promise(function(resolve, reject) {
// 定义异步的内容
setTimeout(function() {
console.log('1s 后输出');
// 输出完成后,调用函数传入的 resolve 函数,将该 promise 实例标记为已完成,当前 promise 串行继续执行
resolve();
}, 1000);
});
}
function promise2() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('2s 后输出'); resolve();
}, 2000);
});
}
上面的两个 promise 实例,串联起来即可写为:promise1().then(function() { return promise2(); });
也可以简写为 promise1().then(promise2);
浏览器中执行之后,即可看到,1s 之后出现 1s 后输出 字样,再经过 2s 出现 2s 后输出 字样。在这个例子中我们能看到。当前 promise 如果状态变为已完成(执行了 resolve 方法),那么就会去执行 then 方法中的下一个 promise 函数。
同样的,如果我们的 promise 变为已拒绝状态(执行了 reject 方法),那么就会进入后续的异常处理函数中。
function promise3() {
return new Promise(function(resolve, reject) {
var random = Math.random() * 10; // 随机一个 1 - 10 的数字
setTimeout(function() {
if (random >= 5) {
resolve(random);
} else {
reject(random);
}
}, 1000);
});
}
var onResolve = function(val) {
console.log('已完成:输出的数字是', val);
};
var onReject = function(val) {
console.log('已拒绝:输出的数字是', val);
}
// promise 的 then 也可以接受两个函数,第一个参数为 resolve 后执行,第二个函数为 reject 后执行
promise3().then(onResolve, onReject);
// 也可以通过 .catch 方法拦截状态变为已拒绝时的
promise promise3().catch(onReject).then(onResolve);
// 也可以通过 try catch 进行拦截状态变为已拒绝的 promise
try {
promise3().then(onResolve);
} catch (e) {
onReject(e);
}
- 这个例子使用了三种方式
拦截
最终变为「已拒绝」
状态的 promise,分别是- 使用 then 的第二个参数
- 使用 .catch 方法捕获前方 promise 抛出的异常
- 使用 try catch 拦截代码块中 promise 抛出的异常
同时我们还可以发现,在改变 promise 状态时调用 resolve 和 reject 函数的时候,也可以给下一步 then 中执行的函数传递参数。这个例子中我们把随机生成的数字传给了 resolve 和 reject 函数,我们也就能在 then 中执行函数的时候拿到这个值。
总结一下本小节的内容:
-
promise 会有三种状态,「进行中」「已完成」和「已拒绝」,进行中状态可以更改为已完成 或 已拒绝,已经更改过状态后无法继续更改(例如从已完成改为已拒绝)。
-
ES6 中的 Promise 构造函数,我们构造之后需要传入一个函数,他接受两个函数参数,执行第一个参数之后就会改变当前 promise 为「已完成」状态,执行第二个参数之后就会变为「已拒绝」 状态。
-
通过 .then 方法,即可在
上一个 promise
达到已完成时继续执行下一个函数或 promise。同时通过 resolve 或 reject 时传入参数,即可给下一个函数或 promise 传入初始值。 -
已拒绝的 promise,后续可以通过 .catch 方法或是 .then 方法的第二个参数或是 try catch 进行捕 获。
三. 如何封装异步操作为 promise
我们可以将任何接受回调的函数封装为一个 promise,下面举几个简单的例子来说明。
// 原函数
function dynamicFunc(cb) {
setTimeout(function() {
console.log('1s 后显示');
cb();
}, 1000);
}
var callback = function() {
console.log('在异步结束后 log');
}
// 用传入回调函数的方式执行
dynamicFunc(callback);
上面的例子就是最传统的,使用传入回调函数的方式在异步结束后执行函数。我们可以通过封装 promise 的方式,将这个异步函数变为 promise。如下:
function dynamicFuncAsync() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('1s 后显示');
resolve();
});
});
}
var callback = function() {
console.log('在异步结束后 log');
}
dynamicFuncAsync().then(function() { callback() });
再举一个例子,发送 ajax 请求也可以进行封装:
function ajax(url, success, fail) {
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
success(this.response); } else {
fail(new Error(this.statusText));
}
};
client.send();
};
ajax('/ajax.json', function() {console.log('成功')}, function() {console.log('失败')});
我们可以看到,调用 ajax 方法时需要传入 success 和 fail 的回调函数进行调用。我们可以不传入回调函 数,通过封装 promise 的方式,在原来的执行回调函数的地方更改当前 promise 的状态,就可以通过链式调用。
function ajaxAsync(url) {
return new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
client.send();
});
};
ajax('/ajax.json')
.catch(function() {
console.log('失败');
})
.then(function() {
console.log('成功');
})
总结一下当前小节:
-
我们可以轻松的把任何一个函数或者是异步函数改为 promise,尤其是异步函数,改为 promise 之 后即可进行链式调用,增强可读性。
-
将带有回调函数的异步改为 promise 也很简单,只需要在内部实例化 promise 之后,在原来执行 回调函数的地方执行对应的更改 promise 状态的函数即可。
四. promise 规范解读
任何符合 promise 规范的对象或函数都可以成为 promise,promise A plus 规范地址: https://promisesaplus.com/
上面我们熟悉了整体 promise 的用法,我们知道了如何去创建一个 promise,如何去使用它,后面我们 也熟悉了如何去改造回调函数到 promise。本小节我们详细过一遍 promise A+ 规范
,从规范层面明白 promise 使用过程中的细节。
4.1 术语
Promise: promise 是一个拥有 then
方法的对象或函数,其行为符合本规范
具有 then 方法(thenable): 是一个定义了 then
方法的对象或函数
值(value): 指任何 JavaScript 的合法值(包括 undefined
, thenable 和 promise)
异常(exception): 是使用 throw
语句抛出的一个值
原因(reason): 表示一个 promise 的拒绝原因。
4.2 要求
1. promise 的状态
一个 Promise 的当前状态必须为以下三种状态中的一种: 等待态(Pending)、已完成(Fulfilled)和已拒绝(Rejected) 。
2.必须有一个 then 方法
一个 promise 必须提供一个 then
方法以访问其当前值和原因。
promise 的 then
方法接受两个参数: promise.then(onFulfilled, onRejected)
他们都是可选参数,同时他们都是函数,如果 onFulfilled
或 onRejected
不是函数,则需要忽略他们。
-
如果 onFulfilled 是一个函数
- 当
promise
执行结束后其必须被调用,其第一个参数为promise
的结果 - 在
promise
执行结束前其不可被调用 - 其调用次数不可超过一次
- 当
-
如果 onRejected 是一个函数
- 当
promise
被拒绝执行后其必须被调用,其第一个参数为promise
的原因 - 在
promise
被拒绝执行前其不可被调用 - 其调用次数不可超过一次
- 当
-
在执行上下文堆栈仅包含平台代码之前,不得调用
onFulfilled
或onRejected
-
onFulfilled
和onRejected
必须被作为普通函数调用(即非实例化调用,这样函数内部this
非严格模式下指向 window) -
then
方法可以被同一个promise
调用多次- 当
promise
成功执行时,所有onFulfilled
需按照其注册顺序依次回调 - 当
promise
被拒绝执行时,所有的onRejected
需按照其注册顺序依次回调
- 当
-
then
方法必须返回一个promise 对象
promise2 = promise1.then(onFulfilled, onRejected);- 只要
onFulfilled
或者onRejected
返回一个值x
,promise 2
都会进入onFulfilled
状态 - 如果
onFulfilled
或者onRejected
抛出一个异常e
,则promise2
必须拒绝执行,并返回拒因e
- 如果
onFulfilled
不是函数且promise1
状态变为已完成,promise2
必须成功执行并返回相 同的值 - 如果
onRejected
不是函数且promise1
状态变为已拒绝,promise2
必须执行拒绝回调并返回相同的据因
- 只要
var promise1 = new Promise((resolve, reject) => {reject() });
promise1
.then(null, function() {
return 123;
})
.then(null, null)
.then(null, null)
.then(
() => {
console.log('promise2 已完成');
},
() => {
console.log('promise2 已拒绝');
});
五. Promise 构造函数上的静态方法
Promise.resolve
返回一个 promise 实例,并将它的状态设置为已完成,同时将他的结果作为传入 promise 实例的值
var promise = Promise.resolve(123);
promise
.then(function(val) {
console.log('已完成', val);
});
// 已完成 123
同样的, Promise.resolve
的参数也可以处理对象,函数等内容,处理方式和上面规范中介绍的相同。
Promise.reject
返回一个 promise 实例,并将它的状态设置为已拒绝,同时也将他的结果作为原因传入 onRejected 函数
var promise = Promise.reject(123);
promise
.then(null, function(val) {
console.log('已拒绝', val);
});
// 已拒绝 123
Promise.all
返回一个 promise 实例,接受一个数组,里面含有多个 promise 实例,当所有 promise 实例都成为已完 成状态时,进入已完成状态,否则进入已拒绝状态。
var promise1 = function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log(1);
resolve();
}, 1000)
});
}
var promise2 = function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log(2);
resolve();
}, 2000);
});
}
Promise.all([promise1(), promise2()])
.then(function() {
console.log('全部 promise 均已完成');
});
注意,此时多个 promise 是同时进行的,也就是在上面这个例子中,等待 1s 打印 1 之后,再等待 1s 就 会打印 2 和「全部 promise 均已完成」。
Promise.race
返回一个 promise 实例,接受一个数组,里面含有多个 promise 实例,当有一个 promise 实例状态改变 时,就进入该状态且不可改变。这里所有的 promise 实例为竞争关系,只选择第一个进入改变状态的 promise 的值。
var promise1 = function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log(1);
resolve(1);
}, 1000)
});
}
var promise2 = function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log(2);
resolve(2);
}, 2000);
});
}
Promise.race([promise1(), promise2()])
.then(function(val) {
console.log('有一个 promise 状态已经改变', val);
});
六. 实现 Promise
以下代码非严谨遵守 Promise A+ 规范,仅为demo测试
class MyPromise{
// 1.handleFn:(resolve,reject)=>{}
// 2.搭建框架:
// ①通过实例化调用的的方法:then... ,promise.then()
// ②通过构造函数调用的方法:static resolve... ,Promise.resolve()
constructor(handleFn){
// 状态
this.status = 'pending';
// 值
this.value = undefined;
// fulfilled 已完成回调数组
this.fulfilledList = [];
// rejected 已拒绝回调数组
this.rejectedList = [];
// 执行 handleFn,4.triggerResolve,triggerReject
handleFn(this.triggerResolve.bind(this), this.triggerReject.bind(this));
}
/**
* 定义成功方法,作为Promise 传入的函数体的参数
* 实现PromiseA+状态转换 定义成功参数
*/
triggerResolve(val){
// 因为需要先把 then 里面注册的回调函数获取到,才能知道在 resolve 里面该去执行哪些回调函数;所以通过 setTimeout(()=>,0)的形式,在下一个Event Loop事件循环内,再去执行回调
// 这里的 triggerResolve 函数,虽然是在构造器内 handleFn 执行了,但是 setTimeout 是在下一个事件循环执行的,所以实际上是先执行的 then ,才是 setTimeout 内的代码
setTimeout(() => {
// 在这个里面通过定义 then 方法之后,可以拿到 then 里的回调函数【function () { console.log('resolve')】,从而通过 resolve 的时候,去执行对应的方法
// 如果这里的代码是同步执行的的,不是写在 setTimeout 内,那么此时的状态已经改变了
// 实际执行的时候,还没完全的执行完,所以要判断下
if(this.status !== 'pending') return;
// 判断 resolve 方法传入的结果是不是 promise
if(val instanceof MyPromise){
// 是promise,则需要给它定义一个 promise, 它的状态就会改变当前所有的状态
val.then(
value => {},
err => {}
)
}else{
// resolve 方法传入的是普通值
// 把当前 promise 的状态变为【已完成】
this.status = 'fulfilled';
// 同时把值记录下来
this.value = val;
// 然后再触发一下,之前在上一个事件循环里面记录的所有回调
this.triggerFulfilled(val);
}
}, 0);
}
/**
* 定义失败方法,作为Promise 传入的函数体的参数
* 实现PromiseA+状态转换 定义失败参数
*/
triggerReject(val){
// 自己写的
setTimeout(() => {
if (this.status !== 'pending') return;
if(val instanceof MyPromise){
val.then(
value => {},
err => {}
)
} else{
this.status = 'rejected';
this.value = val;
this.triggerRejected(val);
}
},0);
}
/**
* 将所有已完成状态的回调执行
*/
triggerFulfilled(val){
// 拿到 this 上定义的所有已完成的回调,以 forEach 的形式执行一下
this.fulfilledList.forEach(item => item(val));
this.fulfilledList = [];
}
/**
* 将所有已拒绝状态的回调执行
*/
triggerRejected(val){
this.rejectedList.forEach(item => item(val));
this.rejectedList = [];
}
/**
* 3.在 then 里面将异步函数【回调函数】注册进去,再在调用 resolve【triggerResolve】 的时候执行了下
* @param onFulfilled 注册的在异步执行的过程中,要去执行的状态改变时的回调函数
* @param onRejected 注册的在异步执行的过程中,要去执行的状态改变时的回调函数
* @returns 每一个then 都要返回一个 Promise
*/
then(onFulfilled, onRejected){
const {status,value} = this;
// 每一个then 都要返回一个 Promise
const promiseInstance = new MyPromise((onNextFulfilled, onNextRejected) => {
/**
* 为了链式调用下去
* @param val triggerFulfilled(val)的时候,去执行的,当前变化的值的时候,改变的这个参数
* @description
* ① 为了将 onFulfilled 与 onNextFulfilled 串联起来;当执行 onFulfilled 函数的时候,就知道怎么去执行 onNextRejected
* ② 在记录回调函数的过程中,通过内部闭包函数的形式,把【上一个 then 里面注册的回调函数】与【 返回的新 promise 的 resolve函数】进行融合。最终要执行的回调函数的结果
* ③ 在执行 onFinalFulfilled 过程中,可以把当前新创建的 promise【 new MyPromise((onNextFulfilled, onNextRejected)=>{})】的 resolve【onNextFulfilled】函数进行执行
* ④ 当上一个注册的回调函数执行的过程中,我们就可以执行下一个 promise 的结果
*/
function onFinalFulfilled(val){
if(typeof onFulfilled !== 'function'){
// 什么时机去执行下一个 promise 的结果?
// 规范里写的:如果上一个 promise 返回的结果不是函数,就直接去把上一个 promise 返回的结果放到下一个 promise 里面去执行
onNextFulfilled(val);
} else {
// 先执行一下 上一步的promise里面的结果,就是res
const res = onFulfilled(val);
if(res === promiseInstance){
throw new TypeError('不能是同一个promise')
}
// 判断返回的 是否为 promise
if(res instanceof MyPromise){
// 通过 promise.then 的方法,注册 onNextFulfilled 与 onNextRejected 这两个回调
// 怎么去执行下一个 then 里面的结果?
// 通过 res 的状态:
// ① 如果 res是 resolve 状态,也就是已完成状态,那么就去执行下一个promise的resolve状态【onNextFulfilled】
// ② 如果 res是 reject 状态,也就是已拒绝状态,那么就去执行下一个promise的reject状态【onNextRejected】
res.then(onNextFulfilled, onNextRejected);
} else {
// 返回一个普通的值,通过调用下一个 promise 的结果
onNextFulfilled(val);
}
}
}
function onFinalRejected(error){
if(typeof onRejected !== 'function'){
onNextRejected(error);
}else{
let res = null;
try {
res = onRejected(error);
} catch (e) {
// 如果在这一步 catch 到了问题,执行下一个 promise链的时候,把这一步暴露的问题传递出去;
onNextRejected(e);
}
// 否则的话,这个还是一个 resolve 的状态
if(res instanceof MyPromise){
res.then(onNextFulfilled, onNextRejected);
}else{
onFulfilled(res);
}
}
}
// 根据不同的 promise状态,执行不同的方法
switch (status) {
case 'pending':{
// 需要记录下当前 注册进来的这两个回调函数
this.fulfilledList.push(onFinalFulfilled);
this.rejectedList.push(onFinalRejected);
break;
}
case 'fulfilled': {
onFinalFulfilled(value);
break;
}
}
})
return promiseInstance;
}
catch(onRejected){
return this.then(null, onRejected)
}
/**
* MyPromise.resolve 实现
*/
static resolve(value){
// 如果返回的值是 promise,则直接将其返回
if(value instanceof MyPromise) return value;
// 如果不是,则返回一个 promise 实例,并将它的状态设置为已完成,同时将它的结果value 作为传入 promise 实例的值
return new MyPromise(resolve => resolve(value));
}
/**
* MyPromise.reject 实现
*/
static reject(reason){
if(reason instanceof MyPromise) return reason;
return new MyPromise((resolve,reject) => reject(reason))
}
/**
* MyPromise.all 实现
*/
static all(list){
return new MyPromise((resolve,reject) => {
if(!Array.isArray(list)) {
return reject(new Error('请传入数组'))
}
let couter = 0
let values = [];
// 此处 for of 是并行的;但在 async await 中for of 可能会出现异步迭代,依次执行
// 此处还可以用 forEach,for...
for (const [i, MyPromiseInstance] of list.entries()) {
MyPromise.resolve(MyPromiseInstance)
.then(
res => {
values[i] = res;
couter++;
if(couter === list.length) resolve(values);
},
err => {
reject(err)
}
)
}
})
}
/**
* MyPromise.race 实现
*/
static race(list){
return new MyPromise((resolve,reject) => {
if(!Array.isArray(list)) {
return reject(new Error('请传入数组'))
}
list.forEach((item,i) => {
// 当某一个promise进入 resolve 状态之后,就进入then
MyPromise.resolve(item)
.then(
res => {
resolve(res)
},
err => {
reject(err)
}
)
})
})
}
}
// 实例化过程中 new Promise 执行顺序:
// ① 先实例化,执行 handleFunc,再进入 then 里面 ,把回调函数注册进来
// ② 这个时候才会去到 triggerResolve 的 setTimeout()里面去
const promise1 = new MyPromise(function(resolve,reject) {
// 注意:进入 then的时候就是 pending状态!!!
// 由于对于 此处的 unction(resolve,reject){} 函数来说,一开始会去立刻执行的,但是它的 resolve函数还没有注册进来,所以在注册的时候它还是 pending状态
// 注册完成之后,在下一个事件循环内执行 resolve 里的方法
resolve()
// reject()
})
promise1.then(function () { console.log('resolve') })
七. 补充
function sleep(time = 1) {
return new Promise(resolve => setTimeout(function() {console.log('promise resolve'); resolve()}, time * 1000))
}
const promiseCreatorList = [
sleep,
sleep,
sleep
]
/********************************************************************/
// 1. Promise.all 并行执行
console.log('Promise.all start', new Date().getTime())
Promise.all(
promiseCreatorList.map(item => item())
).then(
() => console.log('Promise.all end', new Date().getTime())
)
/********************************************************************/
// 2. for of async 迭代依次执行
async function main() {
console.log('for of async start', new Date().getTime())
async function forOfLoop() {
for (const promiseInstance of promiseCreatorList) {
await promiseInstance()
}
}
await forOfLoop()
console.log('for of async end', new Date().getTime())
}
main()
/********************************************************************/
// 3. 把并行的 Promise 实现串联的
const promiseChain = promiseCreatorList.reduce((memo, current)=>{
if(memo.then){
return memo.then(current)
}
return memo().then(current)
})
// 相当于 sleep().then(sleep).then(sleep )
promiseChain.then(function () {
console.log('已完成')
})
/********************************************************************/
// 4. 通过 Promise 实现依次执行
const promiseChain2 = promiseCreatorList.reduce((memo, current)=>{
return memo().then(current)
}, Promise.resolve())
// 相当于 Promise.resolve().then(sleep).then(sleep).then(sleep)
promiseChain2.then(function () {
console.log('已完成')
})