Promise基本介绍
Promise的出现, 为解决异步回调时, 避免回调地狱提供了更直观的处理方案, 是比回调函数更直观和强大的原生api。 相关规范本身在 ES6之前就已经存在了, 只是在ES6中得以标准化, 成为了语言标准中的一部分。
它主要有以下两个特点:
(1)对象的状态不受外界影响 。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。
基本用法
下面简单介绍一下 基本用法。
// basic
const promise = new Promise(function(resolve, reject) {
// ...code 此处应该是一个异步操作, 并通过异步操作得到一个结果 ,用于判断成功 或者失败
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
}).then(res => {
console.log(res); // success callback
}, err=>{
throw err; // failed callback
});
Promise除了可以通过 then 的第二个参数进行异步错误回调以外, 也可以通过 catch方法进行报错捕获。 而 finally方法,则不管回调是成功还是失败都会在 最后执行。
除此简单应用以外, 还有 静态方法 all, race , allSettled , any等等。 本篇文章只实现最基本的all方法, 其他静态方法 大同小异,不作赘述。
作为一个初入前端的精神小伙, 不禁对其实现产生了兴趣。 于是收集了一下 Promise 规范中的部分要求, 并尝试手动实现相关的功能。
Promise A+ 规范
以下列出规范中提出的若干点, 更详细的内容 可以查阅 Promise A+ 规范。
1. 基本特征
-
Promise是一个通过构造函数生成的对象,对象应该接收一个函数作为参数, 且函数的两个参数应该分别是
resolve和reject函数(用以提醒内部异步是否成功或失败, 同时修改内部的状态)。 -
此对象原型上应该有一个
then方法, 且应该有两个参数,分别是两个回调函数, 分别作为成功 和失败的回调。 -
内部应该有一个
value属性表示成功回调中的变量 , 有一个reason属性表示失败回调中的变量。
2. 必须要求
-
一个
Promise中的state状态属性必须是pending,fulfilled,rejected中的一个。且默认妆台为pending。 除了pending状态可以变成另外两个状态以外, 另外两个状态不能相互改变。 -
状态改变只能在
resolve或者reject函数中进行, 且状态本身外界无法修改。 -
then方法支持两个参数,且都是函数, 分别是成功和失败的回调。如果对应的回调函数(参数)没有传, 必须被省略。promise.then([onFulfilled, onRejected])
一点一点来写吧!
通过以上描述可以 比较简单的搭一个 构造函数的框架( 用的class因为代码少 , ES5中实现一样的,只要分清 原型和内部变量即可 )
;(function(global){
const STATUS_PENDING = Symbol("pending");
const STATUS_FULFILLED = Symbol("fulfilled");
const STATUS_REJECTED = Symbol("rejected");
class Promise2 {
constructor(executor){
this.state = STATUS_PENDING; // 2.1
this.value = undefined; // 1.3
this.reason = undefined; // 1.3
let resolve = (res)=>{
if(this.state === STATUS_PENDING){ // 2.2
this.state = STATUS_FULFILLED;
this.value = res;
}
}
let reject = (err)=>{
if(this.state === STATUS_PENDING){ // 2.2
this.state = STATUS_REJECTED;
this.reason = err;
}
}
executor(resolve, reject); // 1.1
}
then( onFulfilled, onRejected ){ // 1.2 2.3
}
}
global.Promise2 = Promise2; // 用于跟 Promise作比较, 防止替换
})(window)
Promise.prototype.then() 实现
由于 then方法第一个参数(onFulfilled)是 成功的回调, 因此它应该在 resolve执行完毕后执行, 而resolve执行完毕的判断依据就是 state 变成了 fulfilled。同时应该将 resolve中修改后的 value当作自己的 参数传给onFulfilled。第二个参数(onRejected)同理。
class Promise2 {
...
then( onFulfilled, onRejected ){
if(this.state === STATUS_FULFILLED) {
onFulfilled(this.value);
}
if(this.state === STATUS_REJECTED) {
onRejected(this.reason);
}
}
}
假设执行以下代码:
new Promise2((resolve, reject)=> {
console.log('start')
resolve('ss')
}).then(res => {
console.log(res);
})
控制台打印如下:
start
ss
在继续写后面内容之前, 我需要捋一下 上述调用到底做了哪些事。
明确一点,以上测试代码 都是同步的, 当按照 调用栈的内容来看待以上的执行过程时, 大概是这样的
-
首先new 了一个
Promise2对象, 并按要求传入了一个 函数作为参数, 此函数在执行到executor(resolve, reject);时,executor函数进入栈底。 -
进程进入
executor函数内部开始执行内部代码, 并打印了start。此时同步代码中接着执行resolve('ss')。 于是resolve进入函数调用栈,此时位于倒数第二层,同时也算是顶层。 -
进程进入
resolve函数内部开始执行内部代码, 修改了状态后, 将传入的字符串ss赋值给了this.value。 至此resolve执行完毕, 并出栈。 -
execurtor执行完毕, 并出栈, 栈空。 此时 new出的Promise2对象 创建完毕。 -
接着后面紧跟着
then方法, 此方法入栈。 内部通过判断state得知,当前的状态为fulfilled。 因此 执行第一个参数,同时将value传给此方法当作回调函数的参数。 -
此回调函数定义
then方法中的第一个参数, 即第一个函数。 此时入栈, 并执行console.log(res)。 于是打印出了ss。 -
回调函数出栈,
then函数出栈,调用栈空, 以上代码执行完毕。
至此,大概知道同步代码整个执行过程是什么样子了。 但是还远远不够,假设将 excurtor改成异步的函数时, 代码如下执行:
new Promise2((resolve, reject)=> {
console.log('start')
setTimeout(()=>{
resolve('ss')
},0)
}).then(res => {
console.log(res);
})
结果只打印了 start 。 then里的回调并没有执行, 原因很简单, 由于setTimeout 把resolve放在当前同步代码后执行, 执行到then时, 此时的 resolve还没执行, 于是状态还保留在pending状态,而这个状态我们并没有考虑应该如何处理。
于是我们需要实现一个功能: 如果执行到then时, resolve或者reject还么有被执行说明,当前还在等待状态, 需要将then的各自回调存储起来, 等到resolve或reject执行时, 再遍历对应的回调集合 并执行。按照这个思路 我们需要定义两个新的变量来存储 各自的回调。改一下代码如下
...
class Promise2 {
constructor(excurtor) {
this.state = STATUS_PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledArr = []; // then 的 成功回调 集合
this.onRejectedArr = []; // then 的 失败回调 集合(或catch)
let resolve = (res)=>{
if(this.state === STATUS_PENDING){ // 2.2
this.state = STATUS_FULFILLED;
this.value = res;
this.onFulfilledArr.forEach(fn => fn()); // resolve执行, 状态 改变为成功, 遍历成功的数组 ,并执行刚才存起来的所有 onFulfilledFn集合
}
}
let reject = (err)=>{
if(this.state === STATUS_PENDING){ // 2.2
this.state = STATUS_REJECTED;
this.reason = err;
this.onRejectedArr.forEach(fn => fn()); // reject 状态 改变为失败, 遍历失败的数组 ,并执行刚才存起来的所有 onRejectedFn集合
}
}
executor(resolve, reject); // 1.1
}
then( onFulfilled, onRejected ){ // 1.2 2.3
if(this.state === STATUS_FULFILLED) {
onFulfilled(this.value);
}
if(this.state === STATUS_REJECTED) {
onRejected(this.reason);
}
if(this.state === STATUS_PENDING) {
this.onFulfilledArr.push(()=>{ onFulfilled(this.value) }); // 状态为pending, 将成功回调存起来
this.onRejectedArr.push(()=>{ onRejected(this.reason) }); // 状态为pending, 将失败回调存起来
}
}
}
此时再去测试 以上 setTimeout例子。 我们先在控制台 打印了start , 在 1秒后, 控制台打印了 ss。 延迟执行回调功能实现, 此时一个最基本的 异步行为完成。
如果对设计模式比较熟悉的人, 可能已经看出来了,其实这里是用到了一个 订阅发布者模式。
但是!! 这还远远不够!!
按照 Promise A+ 的规范 , 关于 excurtor和 then方法还有以下几点需要实现:
-
excurtor内部如果报错, 需要被reject捕获。 -
then方法需要返回一个Promise。同时支持链式调用。 -
如果
then方法没有接收到函数作为参数, 则需要将结果当作新返回的Promise中resolve的回调结果。 即 值的穿透。 -
需要区别 宏任务和微任务, 在有宏任务和
Promise和同步代码同时执行时,Promise的表现应该与微任务想通过。
开始新的分析。
报错需要被 reject捕获,不难联想到捕获错误的 try catch, 其实就是在catch块中, 将错误 给到reject。我们稍微改一下 constructor结尾代码
constructor(excurtor){
...
try{
excurtor(resolve, reject);
}catch(err){
reject(err) // 如果报错直接 通知给 reject, 进入 错误回调
}
}
then需要返回一个 Promise, 同时需要支持链式调用,实质就是 返回一个已经执行了 resolve的 Promise, 因为只有这样才能把前者的结果当作结果传给新的Promise的then。
同时值得穿透实际上就是当发现没有给回调参数时, 给个默认值回调, 并把结果传给新的 Promise的resolve。再由于, 此处的返回的 Promise中的resovle执行的是 使用者传入的一个函数,此函数不在内部可控范围内, 那么也就不能保证一定不报错, 为了能捕获到对应的错误, 此处我们也需要通过try catch来进行修改。
我们的then方法修改如下。
...
then( onFulfilled, onRejected){
onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : res => res;
onRejected = typeof onRejected == 'function' ? onRejected : err => { throw err}; // 由于 throw err 是一个语句, 不能被return, 所以需要用大括号包裹起来
return new Promise2((resolve, reject)=>{
if(this.state === STATUS_FULFILLED) {
try {
resolve(onFulfilled(this.value));
}catch(err){
reject(err);
}
}
if(this.state === STATUS_REJECTED) {
try {
reject(onRejected(this.reason));
}catch(err){
reject(err);
}
}
if(this.state === STATUS_PENDING) {
this.onFulfilledArr.push(()=>{
try{
resolve(onFulfilled(this.value))
}catch(err){
reject(err)
}
});
this.onRejectedArr.push(()=>{
try {
reject(onRejected(this.reason))
}catch(err){
reject(err)
}
});
}
})
}
至此then的链式调用,和值的穿透就已经实现了, 那么我们接着来测试一下它的异步问题。
new Promise2((resolve)=>{
console.log('start');
resolve('three')
}).then(res => {
console.log(res)
})
console.log('second')
打印如下:
start
three
second
明显处理有问题,因为即使传入的函数体是一个同步代码, then函数内部的回调也应该是一个异步回调。 那么我们还需要将 then中的回调改成异步的 , 先用 setTimeout实现。
function observeCallback(callback){
setTimeout(()=>{
callback();
},0)
}
...
then( onFulfilled, onRejected){
onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : res => res;
onRejected = typeof onRejected == 'function' ? onRejected : err => { throw err}; // 由于 throw err 是一个语句, 不能被return, 所以需要用大括号包裹起来
return new Promise2((resolve, reject)=>{
if(this.state === STATUS_FULFILLED) {
observeCallback(()=>{
try {
resolve(onFulfilled(this.value));
}catch(err){
reject(err);
}
})
}
if(this.state === STATUS_REJECTED) {
observeCallback(()=>{
try {
reject(onRejected(this.reason));
}catch(err){
reject(err);
}
})
}
if(this.state === STATUS_PENDING) {
this.onFulfilledArr.push(()=>{
observeCallback(()=>{
try{
resolve(onFulfilled(this.value))
}catch(err){
reject(err)
}
})
});
this.onRejectedArr.push(()=>{
observeCallback(()=>{
try {
reject(onRejected(this.reason))
}catch(err){
reject(err)
}
})
});
}
})
}
以上代码比较简单, 就是编写了一个observeCallback方法, 此方法内部 立即执行一个 setTimeout方法,变为异步执行。 此时再测试上述的代码 ,表现正常
start
second
three
但是setTimeout在 异步任务中, 属于宏任务,并不是微任务。我们常见的宏任务 有 setTimeout, 微任务 有 process.nextTick(nodejs环境)、 MutationOberseve(DOM监听)。由于我们是在浏览器端使用,所以通过MutationObserve来实现微任务下的异步行为。
于是让我们来重写一个 observeCallback方法:
function observeCallback(callback){
let randomStr = Math.floor(Math.random()*1000)+''; // 隐式类型转换 =》 String
let textNode = document.createTextNode(randomStr); // 创建一个DOM => TextNode =》 文本节点 =》 开销小于 元素节点
let observe = new MutationObserver(()=>{
callback()
})
observe.observe( textNode, {
characterData: true // 如果节点中的文本发生变化就会触发 回调 监听
})
textNode.textContent = randomStr + 'promise'; // 这里只是为了 修改已有TextNode中的 文本内容, 用来手动触发 mutationObserve 中的回调 手动触发监听事件
}
我们通过代码测试一下:
setTimeout(()=>{
console.log('three')
},0)
new Promise2((resolve, reject)=>{
console.log('start');
resolve('second')
}).then(res=>{
console.log(res)
})
console.log('four')
此时以上控制台打印如下:
start
four
second
three
打印顺序正常, then方法实现完毕。
Promise.resolve() 方法实现
Promise.resolve()这个调用的样子就不难看出, resolve方法是Promise内部的一个静态方法, 关于此方法的描述大概是这样的:
resolve方法接收一个任意值, 并对此值解析成一个Promise对象。- 如果值是
Promise则返回这个Promise - 如果这个值是
thenable(对象中带有then方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态
让我们接着写
class Pormise2 {
constructor(excurtor){
...
let resolve = (res)=>{
if(res instanceof Promise2) {
return res;
}
if(this.state === STATUS_PENDING){
this.state = STATUS_FULFILLED;
this.value = res;
this.onFulfilledArr.forEach(fn => fn());
}
}
}
static resolve(value){
return new Promise((res, rej)=>{
try {
res(value);
}catch(err){
rej(err);
}
})
}
}
关于如何理解 thenable这一点,我没太看懂 ,所以只列了出来。并未实现,知乎用户齐小神的实现版本中, 有对then的处理,感兴趣的可以去看看。
Promise.reject() 方法实现
...
static reject(data){
return new Promise((res, rej)=>{
rej(data)
})
}
Promise.prototype.catch() 实现
...
catch(err){
return this.then(null , err);
}
Promise.prototype.finally() 实现
finally(callback){
// 当前的 then 和 catch 走完了 才走,所有要返回当前的 then ,此时的 then 实质 是 内部 在调用链上加的一个 新的then,属于在 执行finally之前执行的一个then, 所以
// 只要没报错, 按照 then的 值得穿透, 这里得 then必能拿到, 在这个then中同时把值返回,则finally必能获取到 。
return this.then(res => {
return new Promise2.resolve(callback()).then(()=> res)
}, err => {
return Promise2.resolve(callback()).then(()=>{throw err})
})
}
Pormise.all() 实现
all方法接受一个可迭代的集合, 可以是常规值, 也可以是Promise的集合。
- 如果是
Promise集合时,只有当所有的Promise中的resolve都执行后, 才会触发all的完成行为。 - 如果
Promise集合中,任意一个执行了reject, 则执行all的失败回调。
static all(iterator){
if(typeof iterator[Symbol.iterator] != 'function'){
// 没有 iterator 接口 , 直接 抛错
throw new TypeError("the data have not iterator . ")
}
// 所有 执行完毕 才 完毕, 一个执行错误 就错误
let resultArr = [];
let index = 0;
return new Promise2((resolve, reject)=>{
try{
iterator = Array.of(...iterator);
let processPromiseResolve = (value)=>{
resultArr[index] = value;
// 判断情况
if(++index >= iterator.length){
resolve(resultArr);
}
}
for(let i=0, len = iterator.length ; i<len ; i++){
let value = iterator[i];
index = i;
if(value && typeof value.then == 'function'){
// 说明有then方法 , 假设是 Promise2 这里不直接 判断是否是 Promise2是由于, 自己写的应该能跟 原生实现的来回调用而不出差错,如果这里写死, Promise将会进入错的判断分支
// 手动监听 Promise的回调
value.then(res=> {
// Promise
processPromiseResolve(res, i)
}, reject)
}else{
// 非 Promise
processPromiseResolve(value,i);
}
}
}catch(er){
reject(er);
}
})
}
如有没考虑周到的地方, 欢迎批评指正。