PromiseA+规范
术语
- promise 是一个有then方法的对象或者是函数,行为遵循本规范
- thenable 是一个有then方法的对象或者是函数
- value 是promise状态成功时的值,也就是resolve的参数, 包括各种数据类型, 也包括undefined/thenable或者是 promise
- reason 是promise状态失败时的值, 也就是reject的参数, 表示拒绝的原因
- exception 是一个使用throw抛出的异常值
规范
Promise States
promise应该有三种状态. 要注意他们之间的流转关系.
-
pending
1.1 初始的状态, 可改变.
1.2 一个promise在resolve或者reject前都处于这个状态。
1.3 可以通过 resolve -> fulfilled 状态;
1.4 可以通过 reject -> rejected 状态; -
fulfilled
2.1 最终态, 不可变.
2.2 一个promise被resolve后会变成这个状态.
2.3 必须拥有一个value值 -
rejected
3.1 最终态, 不可变.
3.2 一个promise被reject后会变成这个状态
3.3 必须拥有一个reason
Tips: 总结一下, 就是promise的状态流转是这样的
pending -> resolve(value) -> fulfilled
pending -> reject(reason) -> rejected
then
promise应该提供一个then方法, 用来访问最终的结果, 无论是value还是reason.
promise.then(onFulfilled, onRejected)
-
参数要求
1.1 onFulfilled 必须是函数类型, 如果不是函数, 应该被忽略. 1.2 onRejected 必须是函数类型, 如果不是函数, 应该被忽略.
-
onFulfilled 特性
2.1 在promise变成 fulfilled 时,应该调用 onFulfilled, 参数是value 2.2 在promise变成 fulfilled 之前, 不应该被调用. 2.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)
-
onRejected 特性
3.1 在promise变成 rejected 时,应该调用 onRejected, 参数是reason 3.2 在promise变成 rejected 之前, 不应该被调用. 3.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数)
-
onFulfilled 和 onRejected 应该是微任务
这里用queueMicrotask来实现微任务的调用.
-
then方法可以被调用多次
5.1 promise状态变成 fulfilled 后,所有的 onFulfilled 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onFulfilled的回调) 5.2 promise状态变成 rejected 后,所有的 onRejected 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onRejected的回调)
-
返回值
then 应该返回一个promise
promise2 = promise1.then(onFulfilled, onRejected);6.1 onFulfilled 或 onRejected 执行的结果为x, 调用 resolvePromise( 这里大家可能难以理解, 可以先保留疑问, 下面详细讲一下resolvePromise是什么东西 ) 6.2 如果 onFulfilled 或者 onRejected 执行时抛出异 常e, promise2需要被reject 6.3 如果 onFulfilled 不是一个函数, promise2 以promise1的value 触发fulfilled 6.4 如果 onRejected 不是一个函数, promise2 以promise1的reason 触发rejected
-
resolvePromise
resolvePromise(promise2, x, resolve, reject)7.1 如果 promise2 和 x 相等,那么 reject TypeError
7.2 如果 x 是一个 promsie
如果x是pending态,那么promise必须要在pending,直到 x 变成 fulfilled or rejected. 如果 x 被 fulfilled, fulfill promise with the same value. 如果 x 被 rejected, reject promise with the same reason.7.3 如果 x 是一个 object 或者 是一个 function
let then = x.then. 如果 x.then 这步出错,那么 reject promise with e as the reason. 如果 then 是一个函数,then.call(x, resolvePromiseFn, rejectPromise) resolvePromiseFn 的 入参是 y, 执行 resolvePromise(promise2, y, resolve, reject); rejectPromise 的 入参是 r, reject promise with r. 如果 resolvePromise 和 rejectPromise 都调用了,那么第一个调用优先,后面的调用忽略。 如果调用then抛出异常e 如果 resolvePromise 或 rejectPromise 已经被调用,那么忽略 则,reject promise with e as the reason 如果 then 不是一个function. fulfill promise with x.
手写一个Promise
const PENDING = 'pending';
const FULFULLED = 'fulfilled';
const REJECTED = 'rejected';
class MPromise {
constructor(fn) {
this.status = PENDING;
this.value = '';
this.reason = '';
this.resolveMicroQueueTaskList = [];
this.rejectMicroQueueTaskList = [];
fn(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (this.status === PENDING) {
this.value = value;
this.status = FULFULLED;
}
}
reject(reason) {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
}
}
get status() {
return this._status;
}
set status(newStatus) {
this._status = newStatus;
if (newStatus === FULFULLED) {
this.resolveMicroQueueTaskList.forEach(cb => {
cb()
});
} else if (newStatus === REJECTED) {
this.rejectMicroQueueTaskList.forEach(cb => {
cb()
});
}
}
then(resolve, reject) {
const resolveFunction = resolve ? resolve : (value) => value;
const rejectFunction = reject ? reject : (reason) => reason;
const nextPromse = new MPromise((resolve, reject) => {
const resolveMicroQueueTask = () => {
queueMicrotask(() => {
const x = resolveFunction(this.value);
this.resolveNextPromise(x, resolve);
})
}
const rejectMicroQueueTask = () => {
queueMicrotask(() => {
const y = rejectFunction(this.reason)
this.resolveNextPromise(y, resolve);
})
}
switch (this.status) {
case PENDING: {
this.resolveMicroQueueTaskList.push(resolveMicroQueueTask);
this.rejectMicroQueueTaskList.push(rejectMicroQueueTask);
break;
}
case FULFULLED: {
resolveMicroQueueTask();
break;
}
case REJECTED: {
rejectMicroQueueTask();
}
}
})
return nextPromse;
}
catch(reject) {
this.then(null, reject);
}
resolveNextPromise(x, resolve) {
resolve(x);
}
static resolve(value) {
if(value instanceof MPromise) {
return value;
}
return new MPromise((resolve, reject) => {
resolve(value);
})
}
static reject(value) {
if(value instanceof MPromise) {
return value;
} else {
return new MPromise((resolve, reject) => {
reject(value);
})
}
}
static race (promiseList) {
let promiseListLen = promiseList.length;
return new MPromise((resolve, reject) => {
if(promiseListLen === 0) {
resolve()
}
for(var i = 0; i< promiseList.length; i++){
MPromise.resolve(promiseList[i]).then(res=> {
resolve(res)
}).catch(err => {
reject(err)
})
}
})
}
static all (promiseList) {
let promiseListLen = promiseList.length;
let j = 0;
let promiseValList = [];
return new MPromise((resolve, reject) => {
if(promiseListLen === 0) {
resolve()
}
for(var i = 0; i< promiseList.length; i++){
MPromise.resolve(promiseList[i]).then(res=> {
j++
promiseValList.push(res);
if(promiseListLen === j) {
resolve(promiseValList)
}
}).catch(err => {
reject(err)
})
}
})
}
}
调用方式
链式调用
const promiseA = new MPromise((resolve, reject) => {
setTimeout(() => {
resolve('reject promiseA')
}, 1000);
})
promiseA.then(res => {
console.log('then1 res', res);
}).then(res=> {
console.log('then2 res', res);
}).catch(err => {
console.log('catch err', err)
})
static resolve | reject方法
MPromise.resolve(promiseA).then(res=> {
console.log('res', res)
}).catch(err => {
console.log('err', err)
})
MPromise.then('123').then((res)=> {
console.log('res', res)
})
MPromise.reject('123').catch((res)=> {
console.log('err', res)
})
race | all
const promiseA = new MPromise ((resolve, reject) => {
setTimeout(function() {
resolve(4000)
}, 4000)
})
const promiseB = new MPromise ((resolve, reject) => {
setTimeout(function() {
resolve(3000)
}, 3000)
})
const promiseC = new MPromise ((resolve, reject) => {
setTimeout(function() {
resolve(2000)
}, 2000)
})
// const promiseD = new MPromise ((resolve, reject) => {
// setTimeout(function() {
// resolve(100)
// }, 100)
// })
const promiseD = new MPromise ((resolve, reject) => {
setTimeout(function() {
reject(100)
}, 100)
})
const promiseE = new MPromise ((resolve, reject) => {
setTimeout(function() {
resolve(10)
}, 10)
})
const promiseAsyncList = [promiseA, promiseB, promiseC, promiseD, promiseE]
#### race
MPromise.race(promiseAsyncList).then(res=> {
console.log('res result is', res);
}).catch(err => {
console.log('err result is', err);
})
#### all
MPromise.all(promiseAsyncList).then(res=> {
console.log('res result is', res);
}).catch(err => {
console.log('err result is', err);
})
Promise.all 和 Promise.allSettled 有什么区别?
最大不同:Promise.allSettled永远不会被reject。
Promise.all的痛点
const promises = [
delay(100).then(() => 1),
delay(200).then(() => 2),
Promise.reject(3)
]
Promise.all(promises).then(values=>console.log(values))
// 最终输出: Uncaught (in promise) 3
Promise.all(promises)
.then(values=>console.log(values))
.catch(err=>console.log(err))
// 加入catch语句后,最终输出:3
当需要处理多个Promise并行时,一旦有一个promise出现了异常,被reject了,情况就会变的麻烦。
尽管能用catch捕获其中的异常,但你会发现其他执行成功的Promise的消息都丢失了。所以要么全部成功,要么全部重来
Promise.allSettled
const promises = [
delay(100).then(() => 1),
delay(200).then(() => 2),
Promise.reject(3)
]
Promise.allSettled(promises).then(values=>console.log(values))
// 最终输出:
// [
// {status: "fulfilled", value: 1},
// {status: "fulfilled", value: 2},
// {status: "rejected", value: 3},
// ]
Promise.allSettled情况下,当前promise的状态,没有任何一个promise的信息被丢失。
因此,当用Promise.allSettled时,我们只需专注在then语句里,当有promise被异常打断时,我们依然能妥善处理那些已经成功了的promise,不必全部重来。
面试练习题
## 1.
const test = new MPromise((resolve, reject) => {
setTimeout(() => {
resolve(111);
}, 1000);
}).then((res) => {
console.log(res);
return res; // -->如果没有return res,后面的test.value的值就是undefined
});
setTimeout(() => {
console.log(test.value);
}, 3000)
## 2.
const test = new MPromise((resolve, reject) => {
setTimeout(() => {
reject(111);
}, 1000);
}).catch((reason) => {
console.log('报错' + reason); // 报错111
console.log('catch', test) // catch Promise {<pending>}
});
setTimeout(() => {
console.log('timeout', test);// timeout Promise {<fulfilled>: undefined}
}, 3000)
40道promise && 事件循环机制 && await && async 输出顺序训练题链接
如何中断Promise?
Promise有个缺点,那就是一旦创建就无法取消,所以本质上promise是无法终止的。但是开发过程中会碰到2个需求:
- 中断调用链
- 中断Promise
1. 中断调用链
就是在某个 then/catch 执行之后,不想让后续的链式调用继续执行了。
Promise的then方法接收两个参数:Promise.prototype.then(onFulfilled, onRejected)
若onFulfilled或onRejected是一个函数,当函数返回一个新Promise对象时,原Promise对象的状态将跟新对象保持一致,详见Promises/A+标准。
因此,当新对象保持“pending”状态时,原Promise链将会中止执行。
Promise.resolve().then(() => {
console.log('then 1')
return new Promise(() => {}) // 返回一个一直pending的promise,原promise对象会与此对象保持一致,就会中断不会向下执行
}).then(() => {
console.log('then 2')
}).then(() => {
console.log('then 3')
}).catch((err) => {
console.log(err)
})
2. 中断Promise
注意这里是中断而不是终止,因为 Promise 无法终止,这个中断的意思是:在合适的时候,把 pending 状态的 promise 给 reject 掉。例如一个常见的应用场景就是希望给网络请求设置超时时间,一旦超时就就中断,我们这里用定时器模拟一个网络请求,随机 3 秒之内返回。
function timeoutWrapper(p, timeout = 2000) {
const wait = new Promise((resolve, reject) => {
setTimeout(() => {
reject('请求超时')
}, timeout)
})
return Promise.race([p, wait])
}
Promise中,resolve后面的语句是否还会执行?
会被执行。如果不需要执行,需要在 resolve 语句前加上 return。
Promise中的值穿透是什么?
.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透
当then中传入的不是函数,则这个then传入的值无效,返回的promise的data将会保存上一个的promise.data。这就是发生值穿透的原因。而且每一个无效的then所返回的promise的状态都为resolved。
Promise.resolve(1)
.then(2) // 注意这里
.then(Promise.resolve(3))
.then(console.log)
上面代码的输出是
1
Promise {<fulfilled>: undefined}
使用Promise实现每隔1秒输出1,2,3
const arr = [1, 2, 3]
arr.reduce((p, x) => {
return p.then(() => {
return new Promise(r => {
setTimeout(() => r(console.log(x)), 1000)
})
})
}, Promise.resolve())
实现mergePromise函数
实现mergePromise函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。
const time = (timer) => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timer)
})
}
const ajax1 = () => time(2000).then(() => {
console.log(1);
return 1
})
const ajax2 = () => time(1000).then(() => {
console.log(2);
return 2
})
const ajax3 = () => time(1000).then(() => {
console.log(3);
return 3
})
function mergePromise () {
// 在这里写代码
}
mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log("done");
console.log(data); // data 为 [1, 2, 3]
});
// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
这道题有点类似于Promise.all(),不过.all()不需要管执行顺序,只需要并发执行就行了。但是这里需要等上一个执行完毕之后才能执行下一个。 解题思路:
- 定义一个数组data用于保存所有异步操作的结果
- 初始化一个
const promise = Promise.resolve(),然后循环遍历数组,在promise后面添加执行ajax任务,同时要将添加的结果重新赋值到promise上。
function mergePromise (ajaxArray) {
// 存放每个ajax的结果
const data = [];
let promise = Promise.resolve();
ajaxArray.forEach(ajax => {
// 第一次的then为了用来调用ajax
// 第二次的then是为了获取ajax的结果
promise = promise.then(ajax).then(res => {
data.push(res);
return data; // 把每次的结果返回
})
})
// 最后得到的promise它的值就是data
return promise;
}
使用Promise封装一个异步加载图片的方法
这个比较简单,只需要在图片的onload函数中,使用resolve返回一下就可以了。
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = function() {
reject(new Error('Could not load image at' + url));
};
img.src = url;
});
}
使用Promise实现:限制异步操作的并发个数,并尽可能快的完成全部
有8个图片资源的url,已经存储在数组urls中。
urls类似于['https://image1.png', 'https://image2.png', ....]
而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。
但有一个要求,任何时刻同时下载的链接数量不可以超过3个。
请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。
var urls = [
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
];
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log("一张图片加载完成");
resolve(img);
};
img.onerror = function() {
reject(new Error('Could not load image at' + url));
};
img.src = url;
});
}
// 答案
function limitLoad(urls, handler, limit) {
let sequence = [].concat(urls); // 复制urls
// 这一步是为了初始化 promises 这个"容器"
let promises = sequence.splice(0, limit).map((url, index) => {
return handler(url).then(() => {
// 返回下标是为了知道数组中是哪一项最先完成
return index;
});
});
// 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
return sequence
.reduce((pCollect, url) => {
return pCollect
.then(() => {
return Promise.race(promises); // 返回已经完成的下标
})
.then(fastestIndex => { // 获取到已经完成的下标
// 将"容器"内已经完成的那一项替换
promises[fastestIndex] = handler(url).then(
() => {
return fastestIndex; // 要继续将这个下标返回,以便下一次变量
}
);
})
.catch(err => {
console.error(err);
});
}, Promise.resolve()) // 初始化传入
.then(() => { // 最后三个用.all来调用
return Promise.all(promises);
});
}
limitLoad(urls, loadImg, 3)
.then(res => {
console.log("图片全部加载完毕");
console.log(res);
})
.catch(err => {
console.error(err);
});
promise.catch后面的.then还会执行吗?
会继续执行。
.then, .catch, .finally都可以链式调用,其本质上是因为返回了一个新的Promise实例。
.catch只会处理rejected的情况,并且也会返回一个新的Promise实例。
.catch(onRejected)与then(undefined, onRejected)在表现上是一致的。
事实上,catch(onRejected)从内部调用了then(undefined, onRejected)。
- 如果
.catch(onRejected)的onRejected回调中返回了一个状态为rejected的Promise实例,那么.catch返回的Promise实例的状态也将变成rejected。 - 如果
.catch(onRejected)的onRejected回调中抛出了异常,那么.catch返回的Promise实例的状态也将变成rejected。 - 其他情况下,
.catch返回的Promise实例的状态将是fulfilled。