Loading

使用TypeScript手写Promise(通过官方872个测试)

说明

这篇笔记不会详细讲关于Promise的使用,可以去看我的另一篇博客你不知道的JavaScript——异步编程(中)Promise

编写的Promise遵循Promise/A+规范,可以通过promises-aplus-test的全部872个单元测试。

本篇笔记是一边编写代码一边记录的,所以代码的可读性上可能不是太好,在写完之后花了一点时间优化代码,所以如果有看不懂的地方可以考虑对比最终的代码仓库:YHaoNan/doge_promise

编写构造函数

Promise的构造函数形如:

new Promise((resolve, reject) => {
  resolve("hello, promise")
});

用户需要传入一个回调函数作为Promise的初始化任务,这里我们称作initialTask,Promise会主动向这个回调函数中传入两个方法让用户改变Promise的状态,resolve是对Promise进行决议,reject是对Promise进行拒绝。

无论是resolve还是reject,都需要用户传入一个任意类型的值,resolve需要一个resolveValue代表决议值,reject需要一个reason代表拒绝的原因。

所以我们可以这样编写我们的Promise的构造函数

class DogePromise {
  constructor(
    initialTask: (
      resolve: (resolveValue: any) => void,
      reject: (reason: any) => void
    ) => void
  ) {
    
  }
}

原谅我给我们的Promise取了这样的名字,因为于小狗要当一条舔狗,所以我们的Promise叫DogePromise。Doge系列还有如下项目:doge-admindoge-ui

Promise状态以及初始化任务的执行

Promise代表一个(异步)任务请求的承诺,一个Promise具有三种状态:

  1. pending:等待状态,此时任务的发起者需要等待任务执行完毕
  2. fulfilled:成功状态,fulfilled可以翻译为履行,意思就是这个任务已经成功的完成了
  3. rejected:拒绝状态,此时该任务执行失败,Promise被拒绝。

我们创建一个枚举类来代表这三种状态

enum State {
  PENDING = "pending",
  FULFILLED = "fulfilled",
  REJECTED = "rejected",
}

同时我们在构造器中将初始状态设为pending

this.state = State.PENDING;

Promise的初始化任务需要同步被调用,所以我们直接在构造器中调用它,先给它传递两个空的回调函数,一会再实现。

initialTask(()=>{},()=>{})

状态改变

如何改变

现实当中,如果我对你许下一个承诺,那么我就要从许下承诺那一刻开始去努力实现,此时你在等我兑现诺言,这个状态就是pending;如果我成功履行了我的诺言,那么此时状态就变成了fulfilled;如果我试了之后发现我实在做不到,那么我就要拒绝履行诺言,此时的状态就是rejected

很显然,一个Promise要由pending转向fulfilledrejected,并且它只能转向其中的一个状态,不存在一个承诺既被履行又被拒绝。而且一旦状态发生了转变,就不可再转向其他状态

对于Promise来说,初始化任务就是用来执行承诺的,在初始化任务中可以做一系列努力去执行承诺,它可以通过resolve函数来决议最终是否履行诺言,也可以通过reject函数来直接拒绝履行诺言。

new DogePromise((resolve, reject) => {
  // 为了诺言做一些尝试...
  reject("拒绝履行承诺"); // or resolve('xxx')
});

无论Promise最终成功履行或是拒绝,Promise都要携带一个值交付给外部,如果成功,这个值就是任务执行后的数据,如果拒绝,这个值就是拒绝的原因。

我们使用两个成员变量记录(其实完全可以使用一个,但是我们为了清晰使用两个)

private value: any;
private reason: any;

resolve的语义

请注意,resolve的语义是决议,如果初始化任务调用resolve,那么根据在resolve中传入的决议值的不同,Promise也可能转入不同的状态,不像reject直接转入拒绝状态。

resolve函数把传入的值resolveValue分为三类

  1. Promise:如果resolveValue是一个Promise,那么当前Promise的状态跟随resolveValue的状态
  2. ThenableThenable是一个具有then方法的对象,如果resolveValue是一个thenable,当前Promise调用其then方法并跟随其状态,在后面会详细介绍
  3. 其他值:如果resolveValue是其他任意类型值,Promise转入fulfilled状态,并将resolveValue作为Promise成功后的value

注意,我们以后为了方便叙述,可能说“Promise被决议”,这代表Promise的状态转为fulfilled或rejected,它既包含了使用resolve转换状态的情况,也包含了用reject转换状态的情况

resolve和reject的实现

在这个阶段,我们假设初始化任务只会向resolve中传递非Promise和非Thenable的值,也就是说,resolve只会转入成功状态。

我们先编写一个用于改变当前Promise状态的函数

/**
  * 修改当前Promise的状态
  * @param state 要进入的状态
  * @param valueOrReason 如果要进入fulfilled状态,那么需要一个Promise成功后的结果,如果要进入rejected状态,那么需要一个拒绝的原因
  * @returns 修改是否成功
  */
private changeState(state: State, valueOrReason: any): boolean {
  // 如果当前状态已经不是Pending了,或者尝试转移状态到pending,直接失败
  if (this.state != State.PENDING || state == State.PENDING) return false;

  this.state = state;
  if (this.state === State.FULFILLED) this.value = valueOrReason;
  else this.reason = valueOrReason;
  return true;
}

编写resolve

private resolve(resolveValue: any) {
  this.changeState(State.FULFILLED, resolveValue);
}

编写reject

private reject(reason: any) {
  this.changeState(State.REJECTED, reason);
}

然后我们将它传入给初始化方法

initialTask(this.resolve.bind(this), this.reject.bind(this));

注意,直接将成员方法传递到外部相当于一次方法赋值操作,在js中会丢失this,所以我们要bind,具体可以参考我的另一篇笔记:你不知道的JavaScript——this全面解析(上)

目前的代码如下:

enum State {
  PENDING = "pending",
  FULFILLED = "fulfilled",
  REJECTED = "rejected",
}

class DogePromise {
  private state: State;
  private value: any;
  private reason: any;

  constructor(
    initialTask: (
      resolve: (value: any) => void,
      reject: (reason: any) => void
    ) => void
  ) {
    this.state = State.PENDING;
    initialTask(this.resolve.bind(this), this.reject.bind(this));
  }

  private changeState(state: State, valueOrReason: any): boolean {
    if (this.state != State.PENDING || state == State.PENDING) return false;
    this.state = state;
    if (this.state === State.FULFILLED) this.value = valueOrReason;
    else this.reason = valueOrReason;
    return true;
  }
  private resolve(resolveValue: any) {
    this.changeState(State.FULFILLED, resolveValue);
  }

  private reject(reason: any) {
    this.changeState(State.REJECTED, reason);
  }
}

export default DogePromise;

现在我们的Promise已经可以转换状态了。

import DogePromise from "./doge_promise";

let p = new DogePromise((resolve, reject) => {
  resolve(123);
});

console.log(p);

Promise转成了成功的状态并且携带了成功后的值

尝试两次改变Promise的状态

let p = new DogePromise((resolve, reject) => {
  resolve(123);
  reject(456);
});

被决议后的Promise状态不会再改变

then

Promise最主要的部分就是then,用户如何接收承诺的结果呢?就是通过then方法。

then方法需要传入两个回调,第一个是onFulfilled,当承诺状态为成功时该方法会被回调,第二个是onRejected,当承诺状态为失败时该方法被回调。

要注意的是,无论用户何时调用then方法,无论调用时Promise处于等待状态,还是已经被决议了,then中的两个回调函数都能在正确的时机被回调。意思就是:

  1. 如果调用thenPromise处于等待状态,那么等Promise的状态发生转变时,回调对应的函数
  2. 如果调用thenPromise已经被决议,那么立即回调对应的回调函数

还要注意的一点是,JS有个特殊的异步模型,为了保证回调不会过早或过晚执行,then方法中的回调需要被异步调用,这是为了保证then中的回调被执行的时机一定晚于then函数本身被执行的时机,这个如果对JS异步模型了解不深的可以去看我的另一篇笔记:你不知道的JavaScript——异步编程(上)传统回调模式

我们这样实现then方法:

public then(
  onFulfilled: (value: any) => void,
  onRejected: (reason: any) => void
) {
  // 这个局部函数用来在Promise已经被决议后调用对应回调
  let handleWhenPromiseIsResolved = () => {
    if (this.state === State.FULFILLED) {
      setTimeout(() => onFulfilled(this.value));
    } else if (this.state === State.REJECTED) {
      setTimeout(() => onRejected(this.reason));
    }
  };

  // 如果当前是pending状态,那么就设置一个监听器,当Promise被决议时重新调用处理函数
  // 这里使用了闭包理念
  if (this.state === State.PENDING) {
    this.onPromiseResolvedListener = handleWhenPromiseIsResolved;
  } else {
    // 如果不是pending状态说明Promise已经被决议,直接调用处理函数
    handleWhenPromiseIsResolved();
  }
}

然后我们还要在Promise状态改变的函数中回调onPromiseResolvedListener

private changeState(state: State, valueOrReason: any): boolean {
  if (this.state != State.PENDING || state == State.PENDING) return false;

  this.state = state;
  if (this.state === State.FULFILLED) this.value = valueOrReason;
  else this.reason = valueOrReason;

  // [+] 执行回调
  if (this.onPromiseResolvedListener) this.onPromiseResolvedListener();

  return true;
}

这样我们的then就可以使用了

new DogePromise((resolve, reject) => {
  resolve(123);
}).then(
  (value) => {
    console.log(`value: ${value}`);
  },
  (reason) => {
    console.log(`reason: ${reason}`);
  }
);

延时决议也正常

new DogePromise((resolve, reject) => {
  setTimeout(() => {
    resolve(123);
  }, 200);
}).then(
  (value) => {
    console.log(`value: ${value}`);
  },
  (reason) => {
    console.log(`reason: ${reason}`);
  }
);

处理多个then的情况

Promise规范要求,当Promise的then多次被调用,那么它们的回调函数应该在Promise决议后按顺序被正确的回调。

按照规范,如下代码应该输出1 value: 123\n2 value: 123\n

import DogePromise from "./doge_promise";

let p = new DogePromise((resolve, reject) => {
  setTimeout(() => {
    resolve(123);
  }, 200);
});

p.then(
  (value) => {
    console.log(`1 value: ${value}`);
  },
  (reason) => {
    console.log(`1 reason: ${reason}`);
  }
);

p.then(
  (value) => {
    console.log(`2 value: ${value}`);
  },
  (reason) => {
    console.log(`2 reason: ${reason}`);
  }
);

然而我们的代码只输出了第二个

当我们同步的去决议时,又能按顺序输出两个:

错误的关键在于这行代码:

this.onPromiseResolvedListener = handleWhenPromiseIsResolved;

先来的then设置的监听器会被后来的覆盖,我们应该为每一次then调用保存一个监听器,而非只有一个。提供一个监听器数组,然后在状态改变方法中调用数组中每一个监听器的回调

if (this.state === State.PENDING) {
  this.onPromiseResolvedListeners.push(handleWhenPromiseIsResolved);
} else {
  handleWhenPromiseIsResolved();
}
private changeState(state: State, valueOrReason: any): boolean {
  if (this.state != State.PENDING || state == State.PENDING) return false;
  this.state = state;
  if (this.state === State.FULFILLED) this.value = valueOrReason;
  else this.reason = valueOrReason;

  this.onPromiseResolvedListeners.forEach((cb) => cb());
  return true;
}

之前的代码在异步情况下也能正常执行了

当then的回调不是函数

Promise规范规定,当then的回调不是函数时,忽略。

嘶,原来then的回调可以不是函数,那么我们修改then方法的定义

// 将参数修改为any类型
public then(onFulfilled: any, onRejected: any) {
  let handleWhenPromiseIsResolved = () => {
    // 加上判断参数类型
    if (this.state === State.FULFILLED && typeof onFulfilled === "function") {
      setTimeout(() => onFulfilled(this.value));
    } else if (
      this.state === State.REJECTED && typeof onRejected === "function"
    ) {
      setTimeout(() => onRejected(this.reason));
    }
  };
  if (this.state === State.PENDING) {
    this.onPromiseResolvedListeners.push(handleWhenPromiseIsResolved);
  } else {
    handleWhenPromiseIsResolved();
  }
}

传入非函数值

p.then(null, null);

p.then((value) => {
  console.log(`2 value: ${value}`);
}, null);

非函数值被忽略

职责链模式

resolve函数要将resolveValue分成三类,分别处理,第一类是Promise,第二类是Thenable,第三类是任何其它值。

对于一个决议值x,我们先测试x是否是Promise,如果是就跟踪x的状态,如果不是,判断x是否是thenable,如果是就跟踪其状态,如果不是,就当成普通值处理。

如果把上面的逻辑全部塞进resolve方法中,那么读代码的人就会面临一场灾难,取而代之,我们可以创建这样一个接口,代表决议值处理器,为这三类决议值分别编写处理器,这样我们可以让各个类型的处理代码分离开,而不是完全放到一个函数中。

interface ResolveValueHandler {
  // 该处理器的名称,其实并没什么用
  name: string;

  /**
   * 处理器估计是否可以处理这个决议值
   * @param resolveValue 决议值
   * @returns 如果该处理器认为自己应该可以处理这个决议值,那么返回true,如果该处理器认为自己无法处理这个决议值,返回false
   */
  canResolve(resolveValue: any): boolean;

  /**
   * 尝试处理决议值
   * @param resolveValue 决议值
   * @param changeState 修改状态的函数
   * @param resolve 决议函数,主要用于递归决议
   * @param reject 拒绝函
   * @returns 如果处理成功,返回true,处理失败,返回false
   *      一旦该方法返回true,那么必须保证已经调用了`changeState`或`reject`对Promise进行立即决议,或调用了`resolve`对Promise进行了递归决议
   */
  tryToResolve(
    resolveValue: any,
    changeState: (state: State, valueOrReason: any) => boolean,
    resolve: (resolveValue: any) => void,
    reject: (reason: any) => void
  );
  /**
   * 对于一个决议值,需要先调用处理器的`canResolve`方法,仅当`canResolve`返回true时,可以调用`tryToResolve`方法
   * 只有当`tryToResolve`返回true时,决议成功
   *
   * 调用者应保证调用`tryToResolve`之时,`canResolve`已经得到调用并返回true。
   * 所以`tryToResolve`方法可以默认`resolveValue`已经经过了`canResolve`中的全部校验,可以不再做这些校验了
   */
}

然后按顺序创建三种处理器

const resolveValueHandlerChain = [
  // Promise类型决议值处理器
  new PromiseValueResolveHandler(),
  // Thenable决议值处理器
  new ThenableValueResolveHandler(),
  // 其他类型决议值处理器
  new DefaultResolveValueHandler(),
];

我们使用类似职责链的设计模式,对这些处理器进行调用

private resolve(resolveValue: any) {
  // 遍历职责链
  for (let handler of resolveValueHandlerChain) {
    // 如果处理器估计自己可以处理该决议值
    if (handler.canResolve(resolveValue)) {
      // 尝试处理
      let resolved = handler.tryToResolve(
        resolveValue,
        this.changeState.bind(this),
        this.resolve.bind(this),
        this.reject.bind(this)
      );
      // 如果处理成功,不再做尝试
      if (resolved) {
        break;
      }
    }
  }
}

PromiseValueResolveHandler

PromiseValueResolveHandler极其简单:

class PromiseValueResolveHandler implements ResolveValueHandler {
  name = "PromiseValueResolveHandler";
  canResolve(resolveValue: any): boolean {
    return resolveValue instanceof DogePromise;
  }
  tryToResolve(resolveValue, changeState, resolve, reject): boolean {
    resolveValue.then(
      (value) => {
        resolve(value);
      },
      (reason) => {
        reject(reason);
      }
    );
    return true;
  }
}

canResolve中,我们判断的依据只是它是否是DogePromise的一个实例,这也对我们实现的Promise的通用性方面产生了限制,我们的DogePromise不能与其他Promise实现交叉使用。

tryToResolve中,我们只是简单的使用then方法对也是一个DogePromise的决议值进行状态跟踪,并且在它成功履行时,调用resolve递归处理(因为成功履行时携带的值仍然有可能是Promisethenable),在它失败时立即调用reject来设置拒绝状态。

ThenableValueResolveHandler

ThenableValueResolveHandler的实现稍为复杂。

canResolve方法只是判定了它是不是非空值或未定义值,如果是直接选择不尝试。

class ThenableValueResolveHandler implements ResolveValueHandler {
  name = "ThenableValueResolveHandler";

  canResolve(resolveValue: any): boolean {
    return resolveValue != null && resolveValue != undefined;
  }

  tryToResolve(resolveValue: any, changeState, resolve, reject): boolean {
    if (
      typeof resolveValue === "object" ||
      typeof resolveValue === "function"
    ) {
      let then;
      try {
        then = resolveValue.then;
      } catch (e) {
        reject(e);
        return true;
      }
      if (typeof then === "function") {
        then.call(
          resolveValue,
          (y) => {
            resolve(y);
          },
          (y) => {
            reject(y);
          }
        );
      } else {
        return false;
      }
    } else {
      return false;
    }

    return true;
  }
}

tryToResolve中,严格遵循Promise/A+规范进行处理,具体可以参考规范。

一开始判断决议值是否是objectfunction,如果不是,那么它不可能是一个thenable,直接返回false,尝试决议失败。

如果它是objectfunction,那么尝试访问它的then属性,Promise/A+规范中说如果访问then属性时发出异常,那么使用这个异常来拒绝决议,所以我们用了一个try-catch语句来捕获这个可能发生的异常(这个异常只有可能在用户恶意通过Oject.defineProperty时才可能出现)。

如果成功获取了then属性,那么判断它是否是一个函数,如果不是,则尝试决议失败,交给其他决议值处理器处理,否则和上一个决议值处理器一样,对then的最终状态进行跟踪。

这里唯一要注意的是要用then.call(resolveValue),否则then方法执行时的this也会丢失,要么就用resolveValue.then

DefaultResolveValueHandler

这是默认决议值处理器,当处理器链走到了这里,那么无论如何,该处理器都要产生成功履行的决议了。所以该处理器的实现简单的一批。

class DefaultResolveValueHandler implements ResolveValueHandler {
  name = "DefaultResolveValueHandler";

  canResolve(resolveValue: any): boolean {
    return true;
  }

  tryToResolve(resolveValue: any, changeState, resolve, reject): boolean {
    changeState(State.FULFILLED, resolveValue);
    return true;
  }
}

canResolve无条件返回truetryToResolve直接履行Promise并将决议值作为履行后的value

一波小测试

我们自己编写几个小测试来证明下目前代码的正确性

// 异步的promise
let promiseAsync = new DogePromise((resolve, reject) => {
  setTimeout(() => {
    resolve("promiseAsync");
  }, 200);
});

// 同步的promise
let promiseSync = new DogePromise((resolve, reject) => {
  resolve("promiseSync");
});

// 嵌套的promise
let nestedPromise = new DogePromise((outerResolve, outerReject) => {
  outerResolve(
    new DogePromise((innerResolve, innerReject) => {
      innerResolve("nestedPromise");
    })
  );
});

// thenable
let thenable = {
  then(resolve, reject) {
    resolve("thenable");
  },
};

// 嵌套的thenable
let nestedThenable = {
  then(outerResolve, outerReject) {
    outerResolve({
      then(innerResolve, innerReject) {
        innerResolve("nestedThenable");
      },
    });
  },
};

// 普通值
let normalValue = "normalValue";

// 对于上面每一个变量作为`DogePromise`的决议值进行决议,打印决议结果
[
  promiseAsync,
  promiseSync,
  nestedPromise,
  thenable,
  nestedThenable,
  normalValue,
].forEach((obj) => {
  new DogePromise((resolve, reject) => {
    resolve(obj);
  }).then((value) => {
    console.log(value);
  }, null);
});

全部成功打印

对于顺序不一致,确实就应该是这个顺序,因为下面三个比上面多了几个Promise.then操作,这个操作是在执行resolve(Promise)操作时产生的,每次then操作都是一个异步任务,所以会比之前的几个延后,而promiseAsync又比其他两个多了一个异步,所以它在最后。

异常处理

Promise规范规定,初始化方法可能出现异常,在这里出现的异常直接以异常作为原因拒绝决议

那么修改构造器中的代码即可

try {
  initialTask(this.resolve.bind(this), this.reject.bind(this));
} catch (e) {
  this.reject(e);
  }

then的链式调用

这是我们的手写Promise版图中的最后一块。

Promise规范中允许then进行链式调用,就是then也要返回一个Promise,形状如下:

new Promise((resolve,reject)=>{
  resolve(123)
})
  .then(value=>{
    return 1;
  },null)
  .then(value=>{
    console.log(value)
  },null);

如上代码应该输出1。

尽管我认为看到这的应该不用我说也能明白这个链式调用的执行流程,但我还是要说一下。

首先,then中的两个回调函数只有一个被执行,因为Promise最终只会产生履行或拒绝中的一个不可变的状态,then方法返回一个Promise,它会根据这两个方法中被调用的那个的返回值进行决议。

都写了这么久了,我感觉添加一个这个需求贼简单,直接上代码:

public then(onFulfilled: any, onRejected: any): DogePromise {
  // 直接返回一个Promise
  return new DogePromise((resolve, reject) => {
    let handleWhenPromiseIsResolved = () => {
      if (
        this.state === State.FULFILLED &&
        typeof onFulfilled === "function"
      ) {
        // [+] 修改了这里,对于返回的Promise使用onFulfilled的返回值进行决议
        setTimeout(() => {
          resolve(onFulfilled(this.value));
        });
      } else if (
        this.state === State.REJECTED &&
        typeof onRejected === "function"
      ) {
        // [+] 修改了这里,对于返回的Promise使用onRejected的返回值进行决议
        setTimeout(() => {
          resolve(onRejected(this.reason));
        });
      }
    };
    if (this.state === State.PENDING) {
      this.onPromiseResolvedListeners.push(handleWhenPromiseIsResolved);
    } else {
      handleWhenPromiseIsResolved();
    }
  });
}

完事儿了。

现在把刚刚的代码换成我们的实现

new DogePromise((resolve, reject) => {
  resolve(123);
})
  .then((value) => {
    return 1;
  }, null)
  .then((value) => {
    console.log(value);
  }, null);

结果输出正常

Promise规范还规定,对于then方法回调中出现的异常,需要以这个异常来拒绝then方法返回的Promise。大概就是这样:

new Promise((resolve, reject) => {
  resolve(123);
})
  .then((value) => {
    throw "error";
  }, null)
  .then(null, (reason) => {
    console.log(reason);
  });

这个异常会渗透到下一个then的拒绝处理函数中。再修改我们的代码

public then(onFulfilled: any, onRejected: any): DogePromise {
  return new DogePromise((resolve, reject) => {
    let handleWhenPromiseIsResolved = () => {
      if (
        this.state === State.FULFILLED &&
        typeof onFulfilled === "function"
      ) {
        setTimeout(() => {
          // [+] 修改了这里
          let result;
          try {
            result = onFulfilled(this.value);
            resolve(result);
          } catch (e) {
            reject(e);
          }
        });
      } else if (
        this.state === State.REJECTED &&
        typeof onRejected === "function"
      ) {
        setTimeout(() => {
          // [+] 修改了这里
          let result;
          try {
            result = onRejected(this.reason);
            resolve(result);
          } catch (e) {
            reject(e);
          }
        });
      }
    };
    if (this.state === State.PENDING) {
      this.onPromiseResolvedListeners.push(handleWhenPromiseIsResolved);
    } else {
      handleWhenPromiseIsResolved();
    }
  });
}

测试

new DogePromise((resolve, reject) => {
  resolve(123);
})
  .then((value) => {
    throw "error";
  }, null)
  .then(null, (reason) => {
    console.log(reason);
  });

那么现在,我们的Promise就写完了??

嘶,但别高兴的太早,看这个测试:

new DogePromise((resolve, reject) => {
  resolve(123);
})
  .then(null, null)
  .then((value) => {
    console.log(value);
  }, null);

我们的实现狗屁也没输出,在标准的Promise中,应该将最初的决议值123向下渗透,所以最终应该输出123。

这个原因是因为,当then方法中的成功失败回调不是方法时,我们的代码直接简单的忽略了它们,但其实可不仅仅是忽略它们那么简单,对于非函数的onFulfilled我们需要将onFulfilled携带的值向下传递,对于非函数的onRejected,我们需要将onRejected的原因渗透到then链中的下一个onRejected(其实就是以同样的原因拒绝then方法返回的那个Promise)。

public then(onFulfilled: any, onRejected: any): DogePromise {
  return new DogePromise((resolve, reject) => {
    let handleWhenPromiseIsResolved = () => {
      if (this.state === State.FULFILLED) {
        setTimeout(() => {
          // [+] 将这个判断移到这里,并且向下决议
          if (typeof onFulfilled != "function") {
            resolve(this.value);
            return;
          }
          let result;
          try {
            result = onFulfilled(this.value);
            resolve(result);
          } catch (e) {
            reject(e);
          }
        });
      } else if (this.state === State.REJECTED) {
        setTimeout(() => {
          // [+] 将这个判断移到这里,并且向下拒绝
          if (typeof onRejected != "function") {
            reject(this.reason);
            return;
          }
          let result;
          try {
            result = onRejected(this.reason);
            resolve(result);
          } catch (e) {
            reject(e);
          }
        });
      }
    };
    if (this.state === State.PENDING) {
      this.onPromiseResolvedListeners.push(handleWhenPromiseIsResolved);
    } else {
      handleWhenPromiseIsResolved();
    }
  });
}

其实then函数里有好多共用代码可以抽取,但我有点懒,不抽了。

再次运行刚刚的测试,已经有了结果

官方测试

有一个Promise/A+的官方测试,通过872个单元测试来测试你的Promise是否符合规范,它的github仓库在这:promises-aplus/promises-tests

我们可以通过npm直接安装它

npm install promises-aplus-tests -g

然后我们编写一个测试文件,使用我们的DogePromise实现来跑这个测试:

import PromiseAplusTests from "promises-aplus-tests";
import {DogePromise} from "./doge_promise";

const adapter = {
  resolved(value) {
    return new DogePromise((resolve, reject) => {
      resolve(value);
    });
  },
  rejected(reason) {
    return new DogePromise((resolve, reject) => {
      reject(reason);
    });
  },
  deferred() {
    let dfd: any = {};
    dfd.promise = new DogePromise((resolve, reject) => {
      dfd.resolve = resolve;
      dfd.reject = reject;
    });
    return dfd;
  },
};
PromiseAplusTests(adapter, function (err) {
  console.log(err);
});

有85个测试没跑通

解决没跑通的测试

Same Promise

它的意思是,不能这样:

let p = new Promise((resolve, reject) => {
  resolve(123);
}).then((value) => {
  return p;
});
p.then(null,(reason)=>{
  console.log(reason);
})

这里p等于then返回的Promise,而then返回的Promise又返回p,就相当于一会如果要再有人调用p.then,就要对p本身进行决议。这样造成了循环决议。

只需要对我们的then方法做一些小小的改动

public then(onFulfilled: any, onRejected: any): DogePromise {
  let p = new DogePromise((resolve, reject) => {
  // ... 省略一些代码 ...
        setTimeout(() => {
          let result;
          try{
            result = onFulfilled(this.value);
            //  发现循环,抛出异常,走到下面的`reject`
            if (result === p)
              throw new TypeError(
                "Chaining cycle detected for promise #<Promise>"
              );
            resolve(result);
          } catch (e) {
            reject(e);
          }
        });
      } else if (this.state === State.REJECTED) {
        // ...一样的处理逻辑
      }
    };
    //...
  return p;
}

再次测试,失败数量减少到75

thenable抛出异常

下一个错误是,调用thenable.then时,如果调用过程中抛出异常,那么使用这个异常作为原因reject

tryToResolve(resolveValue: any, changeState, resolve, reject): boolean {
  // ... 省略代码 ...
    let then;
    try {
      then = resolveValue.then;
      // [+] 最主要把这几句移动到try-catch里面了,这样如果then抛出异常那么直接`reject`
      if (typeof then === "function") {
        then.call(
          resolveValue,
          (y) => {
            resolve(y);
          },
          (y) => {
            reject(y);
          }
        );
      } else {
        return false;
      }
    } catch (e) {
      reject(e);
      return true;
    }
  // ...省略代码...
}

错误数还剩60个

thenable resolve两次

老子看这个东西勉强写出了一个和官方Promise行为不符的测试用例

let p = new Promise((resolve, reject) => {
  resolve({
    then(resolve, reject) {
      setTimeout(() => {
        resolve({
          then(resolve2, reject2) {
            // 尝试再度调用外层的resolve
            resolve(1);
          },
        });
      });
    },
  });
});
p.then((value) => {
  console.log(value);
}, null);
console.log(p);

官方的Promise应该什么都不输出,并且p的状态还是pending,就相当于内层调外层已经调用过的resolve时会被忽略。

我们的Promise调用了onFulfilled方法输出了值。

状态是pending是因为onFulfilled方法被异步调用,所以当时确实应该是pending,把输出语句改到里面就是fulfilled

p.then((value) => {
  console.log(value);
  console.log(p);
}, null);

但是不管怎样,我们的代码的确存在内部的thenable能够成功调用外部方法的问题。

我们修改tryToResolve方法,在里面放置一个变量,这个变量记录当前是否已经调用过改变Promise状态的方法了,第二次尝试调用这些方法会静默失败

tryToResolve(resolveValue: any, changeState, resolve, reject): boolean {

  let isAlreadyResolved = false;
  // 该方法会在两个回调没有一个被调用时才调用用户传入的回调,之后更新标志位,下次不会再有人可以调用回调
  function safeCallResolveMethod(method, arg) {
    if (isAlreadyResolved) return;
    isAlreadyResolved = true;
    method(arg);
  }

  // ...省略代码...

      then = resolveValue.then;
      if (typeof then === "function") {
        then.call(
          resolveValue,
          (y) => {
            // 安全调用
            safeCallResolveMethod(resolve, y);
          },
          (y) => {
            // 安全调用
            safeCallResolveMethod(reject, y);
          }
        );
        return true;
      } else {
        return false;
      }
    } catch (e) {
      // 安全调用
      safeCallResolveMethod(reject, e);
      return true;
    }
 

全部通过

Promise.resolve

function resolve(value: any): DogePromise {
  return new DogePromise((resolve, _) => {
    resolve(value);
  });
}

Promise.reject

function reject(reason: any): DogePromise {
  return new DogePromise((_, reject) => {
    reject(reason);
  });
}

Promise.all

function all(promises: DogePromise[]): DogePromise {
  return new DogePromise((resolve, reject) => {
    let fulfilledCnt = 0;
    let fulfilledValues = [];
    promises.forEach((p, i) => {
      setTimeout(() => {
        p.then(
          (value) => {
            fulfilledValues[i] = value;
            fulfilledCnt++;
            if (fulfilledCnt === promises.length) {
              resolve(fulfilledValues);
            }
          },
          (reason) => {
            reject(reason);
          }
        );
      });
    });
  });
}

Promise.race

function race(promises: DogePromise[]): DogePromise {
  return new DogePromise((resolve, reject) => {
    for (let p of promises) {
      setTimeout(() => {
        p.then(
          (value) => {
            resolve(value);
          },
          (reason) => {
            reject(reason);
          }
        );
      });
    }
  });
}

Promise.allSettled

function allSettled(promises: DogePromise[]): DogePromise {
  return new DogePromise((resolve, reject) => {
    let result = [];
    promises.forEach((p, i) => {
      p.then(
        (value) => {
          result.push({
            status: "fulfilled",
            value,
          });
          if (i == promises.length - 1) resolve(result);
        },
        (reason) => {
          result.push({
            status: "rejected",
            reason,
          });
          if (i == promises.length - 1) resolve(result);
        }
      );
    });
  });
}

Promise.any

function any(promises: DogePromise[]): DogePromise {
  return new DogePromise((resolve, reject) => {
    let rejectedReasons = [];
    let rejectedCnt = 0;
    promises.forEach((p, i) => {
      setTimeout(() => {
        p.then(
          (value) => {
            resolve(value);
          },
          (reason) => {
            rejectedReasons[i] = reason;
            rejectedCnt++;
            if (rejectedCnt === promises.length) {
              reject(rejectedReasons);
            }
          }
        );
      });
    });
  });
}

Promise.prototype.catch

public catch(onRejected: any): DogePromise {
  return this.then(null, onRejected);
}

Promise.prototype.finally

finally比较特殊,有两点需要说明

  1. finally的回调函数不接收上层Promise传过来的valuereason
  2. finally回调函数的返回值不渗透给下层
public finally(callback: any) {
  return this.then(
    (value) => {
      if (typeof callback === "function") callback();
      return value;
    },
    (reason) => {
      if (typeof callback === "function") callback();
      throw reason;
    }
  );
}

网上看到的其他人写的版本:

Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}

这个版本是错的,因为它使用Promise.resolvecallback的返回值渗透给了下层。

代码仓库

前期忘记开git,导致git上没有按笔记中的顺序进行版本提交,后面会开的

YHaoNan/doge_promise

posted @ 2021-11-17 15:37  yudoge  阅读(1352)  评论(0编辑  收藏  举报