深入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

执行成功:

相关推荐
辻戋11 分钟前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保13 分钟前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun1 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp1 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.2 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl4 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友6 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理8 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻8 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js