使用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM