深入Promise-3:手撸一个Promise

规范

Promise的实现遵循Promise/A+的规范:promisesaplus.com/

Promise/A+的测试工具:github.com/promises-ap...

Promise的基础

阅读规范,会发现规范中并没有描述Promise的结构,这是可以理解的,因为规范主要规定了promise的状态转换和then方法的行为。至于具体的实现主要看开发者和使用的语言。

Promise的基础使用:

scss 复制代码
new Promise((resolve, reject) => {
  resolve();
  reject();
})
.then()
.catch()
.finally();

分析:

  1. Promise对象的prototype上有三个方法:
    • then
    • catch
    • finally
  2. 对象的construct方法接收一个方法,并且方法会接受两个标准的参数 resolve和reject 两个方法
  3. Promise具备三种状态:pending、fulfilled、reject
  4. Promise状态变更后会产生value或reason,默认值为undefined

代码的基础结构:

ini 复制代码
// 定义三个状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED= 'rejected';

class MyPromise {
    state = PENDING;
    value;
    reason;

    constructor(func) {
        const resolve = (value) => {
            if (this.state === PENDING) {
                this.state = FULFILLED;
                this.value = value;
            }
        }

        const reject = (reason) => {
            if (this.state === PENDING) {
                this.state = REJECTED;
                this.reason = reason;
            }
        }

        try {  // 传入的函数执行错误直接抛出错误
            func(resolve, reject);
        } catch(err) {
            reject(err)
        }
    }
    then() {}
    catch() {}
    finally() {}
}

当然这是最简单的结构,只能实现new操作。

then方法

Promise中then是最核心的方法,catch其实就是then(null, onReject),finally也可以通过then实现:

javascript 复制代码
finally(callback) {
  return this.then(
      value  => resolve(callback()).then(() => value),
      reason => resolve(callback()).then(() => { throw reason })
  );
}

关于then方法的行为,规定也给出了详细的说明:

看起来很复杂,我们一点点分析。

基础信息

Promise必须提供then方法用来访问其value或reason。then方法接收两个方法:onFulfilledonRejected。这两个方法分别是Promise成功的回调和失败的回调:

javascript 复制代码
then(onFulfilled, onRejected) {}

2.2.1 ~ 2.2.3 分别说明了onFulfilledonRejected规范,总结一下:

  • 如果不是方法,直接忽略
  • onFulfilled必须在Promise状态变更为fulfilled之后才能调用,并且接收的第一个参数必须是Promise的值,不能在状态变更为fulfilled之前被调用
  • onRejected必须在Promise状态变更为rejected之后才能调用,并且接收的第一个参数必须是Promise失败的原因,不能在状态变更为rejected之前被调用
  • 回调方法只能被调用一次,不允许超过一次

实现思路:

  • 如果状态为pending,需要等待,可以使用一个变量将函数存储起来,在状态变更后再执行对应的函数(就是在resolve和reject执行的时候执行)
  • 如果状态已经变更,直接执行对应的回调函数
ini 复制代码
class MyPromise {
  // ...
  // 添加存储的变量
  fulfilledCallback = null;
  rejectedCallback = null;
  // ...
  const resolve = (value) => {
      if (this.state === PENDING) {
          this.state = FULFILLED;
          this.value = value;
          fulfilledCallback && fulfilledCallback(this.value); // 执行回调
      }
  }

  const reject = (reason) => {
      if (this.state === PENDING) {
          this.state = REJECTED;
          this.reason = reason;
          rejectedCallback && rejectedCallback(this.reason); // 执行回调
      }
  }
  // ...
  then(onFulfilled, onRejected) {
    if (this.state === PENDING) {
      if (onFulfilled instanceof Function) {
        fulfilledCallback = onFulfilled;
      }

      if (onRejected instanceof Function) {
        rejectedCallback = onRejected;
      }
    } else if (this.state === FULFILLED) {
      if (onFulfilled instanceof Function) {
        onFulfilled();
      }
    } else if (this.state === REJECTED) {
      if (onRejected instanceof Function) {
        onRejected();
      }
    }
  }
}

2.2.4 onFulfilledonRejected 只有在执行环境堆栈仅包含平台代码时才可被调用。这里的"platfom code"指的是引擎、环境和Promise实现代码,其实就是指代码执行的平台对Promise的实现。在实践中,需要保证onFulfilledonRejected是异步执行(相对于then),它们的执行时机应该在调用then的事件循环之后的新的执行任务中。在JavaScript中,使用的是微任务来实现。

这个点说白了就是onFulfilledonRejected必须在当前执行栈清空后才能执行。

这个属于平台底层实现的机制,这里可以用setTimeout来模拟异步的效果(注意:setTimeout是宏任务,promise.then是微任务,这里只是模拟)

2.2.5 onFulfilledonRejected必须被作为函数调用(即没有this)。作为函数调用是指这样的调用方式:

scss 复制代码
onFulfilled();
onRejected();

区别于其他的调用方式:

scss 复制代码
// 作为方法
xx.onRejected();
// 作为构造函数
new onRejected();
// 指定this
onRejected.call(xx);
onRejected.apply(xx)

这些方式都会指定this。

不指定this,那么函数的this就会绑定到全局对象(严谨模式下是undefined),这样不会将当前的Promise实例产生依赖,增加代码的灵活性,同时避免出现副作用(例如回调函数将实例返回,会导致链式调用出现问题)。

2.2.6 then可以被同一个Promise多次调用:

  • Promise成功后,所有的onFulfilled按照注册的顺序依次调用
  • Promise失败后,所有的onRejected按照注册的顺序依次调用

这里指的是这种情况:

typescript 复制代码
const p = new Promise((resovle, reject) => {
  resovle();
});
 
p.then(() => {console.log(1);});  // 1
p.then(() => {console.log(2);});  // 2
p.catch(() => {console.log(3);});
p.catch(() => {console.log(4);});

实现思路: 如果Promise的状态已经变更了,直接执行回调函数即可。如果未变更,将存储回调函数的变量改成数组,收集阶段入列,执行阶段遍历执行

scss 复制代码
// ...
class MyPromise {
  // ...
  fulfilledCallbackList = [];
  rejectedCallbackList = [];

  // ...
  const resolve = (value) => {
    // ...
    fulfilledCallbackList.forEach((fn) => {
      fn(this.value); // 执行回调
    });
  }

  // ...
  then(onFulfilled, onRejected) {
    if (this.state === PENDING) {
      if (onFulfilled instanceof Function) {
        fulfilledCallbackList.push(
          () => {
            setTimeout(() => {
              onFulfilled();
            })
          }
        )
      }

      if (onRejected instanceof Function) {
        rejectedCallbackList.push(
          () => {
            setTimeout(() => {
              onRejected();
            })
          }
        );
      }
    }
    // ...
  }
}

2.2.7 then必须返回一个Promise:

ini 复制代码
promise2 = promise1.then(onFulfilled, onRejected);
  • 返回的promise是一个新的promise
  • 如果onFulfilled或者onRejected返回了值 x,执行Promise的解决过程[[Resolve]](promise2, x)。Promise的解决过程是一个核心的逻辑,会根据x来处理返回值。
  • 如果onFulfilled或者onRejected抛出错误e,promise2的状态为rejected,错误原因为e
  • 如果onFulfilled不是方法并且promise1的状态是fulfilled,promise2的状态必须为fulfilled,使用promise1的value作为返回值
  • 如果onRejected不是方法并且promise1的状态是rejected,promise2的状态必须为rejected,使用promise1的reason作为reason

实现思路:

  • 返回一个新的promise
  • 在函数执行时增加错误捕获,如果有错误直接抛出错误
  • onFulfilledonRejected不是方法的情况下,promise2跟随pormise1的状态,并返回对应的值。将两个参数默认值置为返回promise1的value和抛出promise1的错误
  • 实现一个Promise解决过程方法:resolvePromise

后面有很多地方都需要判断是否为函数,所以我们先增加一个判断函数类型的方法:

javascript 复制代码
function isFunction(func) {
    return typeof func === 'function';
}

这里必须用typeof,如果用instanceof的话,会出现将继承Function.prototype的对象判断为方法的情况,会出现方法无法执行挂掉的情况。

then的代码:

ini 复制代码
then(onFulfilled = null, onRejected = null) {
    onFulfilled = isFunction(onFulfilled) ? onFulfilled : () => this.value;
    onRejected = isFunction(onRejected) ? onRejected : () => { throw this.reason };

    let promise2 = new MyPromise((resolve, reject) => {
        // 等待中,收集回调
        if (this.state === PENDING) {
            this.fulfilledCallbackList.push(
                () => {
                    setTimeout(() => {
                        try {
                            const v = onFulfilled(this.value);
                            resolvePromise(promise2, v, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                }
            );

            this.rejectedCallbackList.push(
                () => {
                    setTimeout(() => {
                        try {
                            const v = onRejected(this.reason);
                            resolvePromise(promise2, v, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                }
            );
        } else if (this.state === FULFILLED) {
            setTimeout(() => {
                try {
                    const v = onFulfilled(this.value);
                    resolvePromise(promise2, v, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        } else if (this.state === REJECTED) {
            setTimeout(() => {
                try {
                    const v = onRejected(this.reason);
                    resolvePromise(promise2, v, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        }
    });

    return promise2;
}

Promise的解决过程

Promise的解决过程是一个抽象的操作,表示为:[[Resolve]](Promise, x),通过上面then的说明,我们知道这个方法接收两个参数,其中Promise是then要返回的新promise(也就是上面代码中的promise2),x为需要返回的值。

接下来我们根据规范实现一个resolvePromise方法。

先看看解决过程的步骤:

从说明中可以总结几个点:

  • promise和x不能是一个,如果是同一个抛出错误,因为promise要返回x,如果是同一个就违反了返回新的promise实例的规则
javascript 复制代码
let p = new Promise((resolve, reject) => {
    resolve();
});
let p2 =  p.then(() => { return p2;}) // TypeError: Chaining cycle detected for promise
  • then都是返回promise2,所以不管x是什么情况,不管中间有多少个层级的嵌套,最终都要改变promise2的状态和对应值或原因。所以resolvePromise需要接收可以修改promise2状态的resolvereject两个方法,否则返回的promise2会一直处于pending的状态。
  • 在所有有返回值的地方全部都需要使用resolvePromise来处理返回值,因为需要x产生的值也可能是promise或thenable。
ini 复制代码
function resolvePromise(promise, x, resolve, reject) {
    // x等于promise,抛出错误
    if (x === promise) {
        reject(new TypeError('Chaining cycle detected for promise'));
    } else if(x instanceof MyPromise) { // 如果是promise
        if (x.state === PENDING) {
            x.then(
              (y) => resolvePromise(promise, y, resolve, reject), 
              e => reject(e)
            );
        } else if (x.state === FULFILLED) {
            resolvePromise(promise, x.value, resolve, reject);
        } else if (x.state === REJECTED) {
            reject(x.reason);
        }
    } else if (x && typeof x === 'object' || isFunction(x)) {
        let called = false; // 标识使用情况
        try {
            let then = x.then;
            if (isFunction(then)) {
                then.call(
                    x, 
                    (y) => {
                        if (called) return;
                        called = true;
                        resolvePromise(promise, y, resolve, reject);
                    },
                    (r) => {
                        if (called) return;
                        called = true;
                        reject(r);
                    }
                )
            } else {
                if (called) return;
                called = true;
                resolve(x);
            }
        } catch(e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

因为resolvePromise存在递归调用,而且不希望resolvePromise被暴露出去,所以这里没将其放在class中。

catch方法和finally方法

这两个方法其实是对then方法的延伸,让Promise的使用变得更简单。

catch的实现比较简单,直接复用then方法就行了

kotlin 复制代码
catch(callback) {
    return this.then(null, callback)
}

finally的实现就比较复杂了,finally的特性如下:

  • 会返回一个新的Promise实例
  • 会将上一个成功或失败的结果透传到后面的方法
  • 如果callback执行失败或者抛出错误,则需要返回错误
typescript 复制代码
const p = new Promise((resolve, reject) => {
    reject(111);
});

p.finally(() => {
    return 333;
}).catch((e) => {
  console.log(e); // 111
});

p.catch(() => {
    return 333;
}).finally(()
.catch((e) => {
  console.log(e); // 333
});

const p = new Promise((resolve, reject) => {
    reject(123);
});

p.finally(() => {
    throw 333;
}).catch((e) => {console.log(e);}); // 333

思路:

  • 创建一个新的Promise,在新的Promise中执行callback,这样可以防止callback影响现有的Promise
  • 在原Promise实例的then中,执行上面的操作,这样就可以保证在原Promise状态变更后都会执行callback
  • 利用then会传递Promise的值和原因的特点,就可以实现透传
  • callback报错需要传递错误,因为是在返回的新Promise中执行,只要callback抛出错误,也会被正常捕获传递
javascript 复制代码
finally(callback) {
    return this.then(
       value => {
         return new MyPromise((resolve) => {
               resolve(callback());
             }).then(() => { 
               return value 
             });
       },
       reason => {
         return new MyPromise((resolve) => {
               resolve(callback());
             }).then(() => {
               throw reason 
             });
       }
    )
}

最终代码

ini 复制代码
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function isFunction(func) {
    return typeof func === 'function';
}

function resolvePromise(promise, x, resolve, reject) {
    // x等于promise,抛出错误
    if (x === promise) {
        reject(new TypeError('Chaining cycle detected for promise'));
    } else if(x instanceof MyPromise) { // 如果是promise
        if (x.state === PENDING) {
            x.then((y) => {
                resolvePromise(promise, y, resolve, reject)
            }, e => reject(e));
        } else if (x.state === FULFILLED) {
            resolvePromise(promise, x.value, resolve, reject);
        } else if (x.state === REJECTED) {
            reject(x.reason);
        }
    } else if (x && typeof x === 'object' || isFunction(x)) {
        let called = false; // 标识使用情况
        try {
            let then = x.then;
            if (isFunction(then)) {
                then.call(
                    x, 
                    (y) => {
                        if (called) return;
                        called = true;
                        resolvePromise(promise, y, resolve, reject);
                    },
                    (r) => {
                        if (called) return;
                        called = true;
                        reject(r);
                    }
                )
            } else {
                if (called) return;
                called = true;
                resolve(x);
            }
        } catch(e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

class MyPromise {
    state = PENDING;
    value;
    reason;
    fulfilledCallbackList = [];
    rejectedCallbackList = [];

    constructor(func) {
        const resolve = (value) => {
            if (this.state === PENDING) {
                this.state = FULFILLED;
                this.value = value;
                // 执行成功回调
                this.fulfilledCallbackList.forEach(fn => fn());
            }
        }

        const reject = (reason) => {
            if (this.state === PENDING) {
                this.state = REJECTED;
                this.reason = reason;
                // 执行失败回调
                this.rejectedCallbackList.forEach(fn => fn());
            }
        }

        try {  // 传入的函数执行错误直接抛出错误
            func(resolve, reject);
        } catch (err) {
            reject(err)
        }
    }

    then(onFulfilled = null, onRejected = null) {
        onFulfilled = isFunction(onFulfilled) ? onFulfilled : () => this.value;
        onRejected = isFunction(onRejected) ? onRejected : () => { throw this.reason };

        let promise2 = new MyPromise((resolve, reject) => {
            // 等待中,收集回调
            if (this.state === PENDING) {
                this.fulfilledCallbackList.push(
                    () => {
                        setTimeout(() => {
                            try {
                                const v = onFulfilled(this.value);
                                resolvePromise(promise2, v, resolve, reject);
                            } catch (e) {
                                reject(e);
                            }
                        });
                    }
                );

                this.rejectedCallbackList.push(
                    () => {
                        setTimeout(() => {
                            try {
                                const v = onRejected(this.reason);
                                resolvePromise(promise2, v, resolve, reject);
                            } catch (e) {
                                reject(e);
                            }
                        });
                    }
                );
            } else if (this.state === FULFILLED) {
                setTimeout(() => {
                    try {
                        const v = onFulfilled(this.value);
                        resolvePromise(promise2, v, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            } else if (this.state === REJECTED) {
                setTimeout(() => {
                    try {
                        const v = onRejected(this.reason);
                        resolvePromise(promise2, v, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            }
        });

        return promise2;
    }

    catch(callback) {
        return this.then(null, callback)
    }

    finally(callback) {
        return this.then(
           value => {
             return new MyPromise((resolve) => {
                   resolve(callback());
                 }).then(() => { 
                   return value 
                 });
           },
           reason => {
             return new MyPromise((resolve) => {
                   resolve(callback());
                 }).then(() => {
                   throw reason 
                 });
           }
        )
    }
}

测试代码

github.com/promises-ap...提供了872测试用例。要使用它来测试,需要先看下要求:

按这里的要求,promise库需要提供一个简单接口适配器,提供几种方法:

  • resolved(value) 创建返回value的promise
  • rejected(reason) 创建返回错误reason的promise
  • deferred() 创建一个由 { promise, resolve, reject } 组成的对象

但是我翻阅了一下他的源码,发现初始化里面有这个方法:

如果提供的适配器,没有resolved和rejected的话,它会自己生成一个,所以我们只要提供deferred方法即可。

按照要求,我们添加这段代码:

ini 复制代码
module.exports = {
    deferred() {
        let dfd = {};
        dfd.promise = new MyPromise((resolve, reject) => {
            dfd.resolve = resolve;
            dfd.reject = reject;
        });
        return dfd;
    }
}

然后,我们需要安装测试工具:

全局安装

npm install promises-aplus-tests -g

或者安装在当前项目

npm install promises-aplus-tests -S -D

在package.json中添加:

json 复制代码
"scripts": {
  "test": "promises-aplus-tests promise.js"
}

执行测试,在promise库的目录下执行命令:

arduino 复制代码
// 全局
promises-aplus-tests promise.js
// 当前项目
npm run test

执行成功:

相关推荐
minDuck4 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!25 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。30 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼36 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093340 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0011 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
子非鱼9212 小时前
【Ajax】跨域
javascript·ajax·cors·jsonp