【JS】因兩道Promise執行題讓我產生自我懷疑,從零手寫Promise加深原理理解


壹 ❀ 引

其實在去年七月份,博客所認識的一個朋友問了我一個關於Promise執行先后的問題,具體代碼如下:

const fn = (s) => (
  new Promise((resolve, reject) => {
    if (typeof s === 'number') {
      resolve();
    } else {
      reject();
    }
  })
  .then(
    res => console.log('參數是一個number'),
  )
  .catch(err => console.log('參數是一個字符串'))
)
fn('1');
fn(1);
// 先輸出   參數是一個number
// 后輸出   參數是一個字符串

他的疑惑是,以上代碼中關於Promise狀態的修改都是同步的,那為什么fn(1)的輸出還要早於fn('1')

說來慚愧,我當時對於這個輸出也疑惑了半天,最后基於自己掌握的現有知識,給了對方一個自認為說的過去但現在回想起來非常錯誤的解釋...想起來真是羞愧= =,這個問題也讓我當時有了了解Promise底層原理的想法。

沒過多久,另一位博客認識的朋友又問了我一道Promise執行順序的題,代碼如下:

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})

// 輸出為0 1 2 3 4 5 6

我看了一眼題,結果難道不應該是0 1 4 2 3 5 6?對方抱着疑問而來,結果這次我自己都蒙圈了,這也讓我意識到自己對於Promise的理解確實有點薄弱。

我承認,上面兩道題真的有點為考而考的意思了,畢竟實際開發我們也不可能寫出像例子2這樣的代碼,但站在面試的角度,對方總是需要一些評判標准來篩掉部分人,人人都不想卷,卻又不得不卷,多懂一點總是沒有壞處。

既然意識到自己的不足,那就花點功夫去了解Promise原理,如何了解?當然是模擬實現一個Promise,所以本篇文章的初衷是通過手寫Promise的過程理解底層到底發生了什么,從而反向解釋上面兩道題為什么會這樣。放心吧,當我寫完我已經恍然大悟,所以你也一定可以,那么本文開始。

貳 ❀ 從零手寫Promise

貳 ❀ 壹 搭建框架

對於手寫新手而言,從零開始寫一個Promise真正的難點在於你可能不清楚到底要實現Promise哪些特性,沒事,我們從一個最簡單的例子開始分析:

const p = new Promise((resolve, reject) => {
  // 同步執行
  resolve(1);
});
p.then(
  res => console.log(res),
  err => console.log(err)
);

從上述代碼我們可以提煉出如下信息:

  1. new過程是同步的,我們傳遞了一個函數(resolve, reject)=>{resolve(1)}Promise,它會幫我們同步執行這個函數。
  2. 我們傳遞的函數接受resolve reject兩個參數,這兩個參數由Promise提供,所以Promise一定得有這兩個方法。
  3. new Promise返回了一個實例,這個實例能調用then方法,因此Promise內部一定得實現then方法。

我們也別想那么多,先搭建一個基本的Promise框架,代碼如下:

class MyPromise {

  constructor(fn) {
    // 這里的fn其實就是new Promise傳遞的函數
    fn(this.resolve, this.reject);
  }

  resolve = () => {}

  reject = () => {}

  then = () => {}
}

constructor中接受的參數fn其實就是new Promise傳遞的函數,我們在constructor中同步調用它,同時傳遞了this.resolvethis.reject,這也就解釋了為何傳遞的函數會同步執行,以及如何使用到Promsise提供的resolve方法。

貳 ❀ 貳 增加狀態管理與值記錄

我們知道Promisepending、fuldilled、rejected三種狀態,且狀態一旦改變就無法更改,無論成功失敗或者失敗,Promise總是會返回一個succesValue或者failReason回去,所以我們來初始化狀態、value以及初步的成功/失敗邏輯:

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {

  constructor(fn) {
    // 這里的fn其實就是new Promise傳遞的函數
    fn(this.resolve, this.reject);
  }
  // 初始化狀態以及value
  status = PENDING;
	value = null;

  resolve = (value) => {
    // 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
    }
  }

  reject = (reason) => {
    // 當調用reject時修改狀態成rejected,同時記錄失敗的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
    }
  }

  then = () => {}
}

叄 ❀ 叄 初步實現then

在實現Promise狀態管理以及值記錄后,我們接着來看看then,很明顯then接受兩個參數,其實就是成功的與失敗的回調,而這兩個函數我們也得根據之前的this.status來決定要不要執行,直接上代碼:

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {

  constructor(fn) {
    // 這里的fn其實就是new Promise傳遞的函數
    fn(this.resolve, this.reject);
  }
  // 初始化狀態以及成功,失敗的值
  status = PENDING;
  value = null;

  resolve = (value) => {
    // 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
    }
  }

  reject = (reason) => {
    // 當調用reject時修改狀態成rejected,同時記錄失敗的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
    }
  }

  then = (fulfilledFn, rejectedFn) => {
    const callbackMap = {
      [FULFILLED]: fulfilledFn,
      [REJECTED]: rejectedFn
    };
    callbackMap[this.status](this.value);
  }
}

那么到這里我們已經實現了一個簡陋的MyPromise,讓我們檢驗下狀態改變以及回調執行:

const p = new MyPromise((resolve, reject) => {
  // 同步執行
  resolve(1);
  reject(2);
});
p.then(
  res => console.log(res),
  err => console.log(err)
);
// 只輸出了1

上述代碼只輸出了1,說明狀態控制以及回調處理都非常成功!!!我們繼續。

貳 ❀ 肆 異步修改狀態

上述代碼雖然運行正常,但其實只考慮了同步resolve的情況,假設我們修改狀態在異步上下文中,就會引發意想不到的錯誤,比如:

const p = new MyPromise((resolve, reject) => {
  // 同步執行
  setTimeout(() => resolve(1), 2000);
});
p.then(
  (res) => console.log(res),
  (err) => console.log(err)
);

Uncaught TypeError: callbackMap[this.status] is not a function

簡單分析下,因為目前我們對於Promise狀態的修改依賴了resolve,但因為定時器的緣故,導致執行p.then執行時狀態其實還是pending,從而造成callbackMap[this.status]無法匹配,因此我們需要添加一個pending狀態的處理。

還有個問題,即使解決了callbackMap匹配報錯,定時器等待結束后執行resolve,我們怎么再次觸發對應回調的執行呢?要不我們在pending狀態中把兩個回調記錄下來,然后在resolve或者reject時再調用記錄的回調?說干就干:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  constructor(fn) {
    // 這里的fn其實就是new Promise傳遞的函數
    fn(this.resolve, this.reject);
  }
  // 初始化狀態以及成功,失敗的值
  status = PENDING;
  value = null;
  // 新增記錄成功與失敗回調的參數
  fulfilledCallback = null;
  rejectedCallback = null;

  resolve = (value) => {
    // 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
      // 新增成功回調的調用
      this.fulfilledCallback?.(value);
    }
  };

  reject = (reason) => {
    // 當調用reject時修改狀態成rejected,同時記錄失敗的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
      // 新增失敗回調的調用
      this.rejectedCallback?.(reason);
    }
  };

  then = (fulfilledFn, rejectedFn) => {
    const callbackMap = {
      [FULFILLED]: fulfilledFn,
      [REJECTED]: rejectedFn,
      // 針對異步問題,新增pending狀態時記錄並保存回調的操作
      [PENDING]: () => {
        this.fulfilledCallback = fulfilledFn;
        this.rejectedCallback = rejectedFn;
      },
    };
    callbackMap[this.status](this.value);
  };
}

再次執行上面定時器的例子,現在不管有沒有異步修改狀態,都能正常執行了!!!

貳 ❀ 伍 實現then多次調用

當我們new一個Promise后會得到一個實例,而這個實例其實是支持多次then調用的,比如:

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 0);
});
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1

但如果我們我們使用自己實現的MyPromise去做相同的調用,你會發現只會輸出1個1,原因也很簡單,我們在pending情況下記錄回調的邏輯只能記錄一個,所以還得再改造一下,將fulfilledCallback定義成一個數組,如下:

class MyPromise {
	// ....
  
  // 修改為數組
  fulfilledCallback = [];
  rejectedCallback = [];

  resolve = (value) => {
    // 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
      // 新增成功回調的調用
      while (this.fulfilledCallback.length) {
        this.fulfilledCallback.shift()?.(value);
      }
    }
  };

  reject = (reason) => {
    // 當調用reject時修改狀態成rejected,同時記錄失敗的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
      // 新增失敗回調的調用
      while (this.rejectedCallback.length) {
        this.rejectedCallback.shift()?.(reason);
      }
    }
  };

  then = (fulfilledFn, rejectedFn) => {
    const callbackMap = {
      [FULFILLED]: fulfilledFn,
      [REJECTED]: rejectedFn,
      // 針對異步問題,新增pending狀態時記錄並保存回調的操作
      [PENDING]: () => {
        this.fulfilledCallback.push(fulfilledFn);
        this.rejectedCallback.push(rejectedFn);
      },
    };
    callbackMap[this.status](this.value);
  };
}

這也修改完成后再次執行上述例子,我們發現多次調用then已滿足。

貳 ❀ 陸 實現then鏈式調用

OK,終於來到Promise鏈式調用這個環節了,對於整個Promise手寫,我個人覺得這部分是稍微有點繞,不過我會盡力解釋清楚,我們先看個最簡單的例子:

const p1 = new Promise((resolve, reject) => {
  resolve(1);
});

p1.then((res) => {
  console.log(res);
  return new Promise((resolve) => resolve(2));
}).then((res) => {
  console.log(res);
});

// 1
// 2

假設我們將上述代碼中的new Promise都改為new MyPromise,運行你會發現代碼直接報錯:

Uncaught TypeError: Cannot read properties of undefined (reading 'then')

不能從undefined上讀取屬性then?我不是在then里面return了一個new Promise嗎?這咋回事?假設你是這樣想的,那么恭喜你,你已經成功進入了思維誤區。

我們將上面的例子代碼進行拆分:

const p1 = new Promise((resolve, reject) => {
  resolve(1);
});

const p2 = p1.then((res) => {
  console.log(res);
  return new Promise((resolve) => resolve(2));
});

p2.then((res) => {
  console.log(res);
});

Promise若要實現鏈式調用,那么p1.then()一定得返回一個新的Promise,不然下一次鏈式調用的then從哪讀取呢?

所以這個新的Promisethen方法創建並提供的,而(res)=>{console.log(1);return new Promise((resolve) => resolve(2))}這一段只是then方法調用時的callback,它的返回值(假設有值)將成為下次新的Promisevalue,所以上述代碼中的return new Promise((resolve) => resolve(2))只是在為then創建的Promise准備value而已。看個例子:

const p1 = new Promise((resolve, reject) => {
  resolve(1);
});

p1.then((res) => {
  console.log(res);
  return new Promise((resolve) => {
    // 我們不改狀態
    console.log("不做狀態改變的操作");
  });
}).then((res) => {
  console.log(res); // 這里不會輸出
});

在這個例子中,第二個then並不會執行,這是因為p1.then()雖然創建了一個新的Promise,但是它依賴的value由內部的new Promise提供,很明顯我們並未做任何狀態改變的操作,導致第二個Promise不會執行。

那么到這里我們能提煉出兩個非常重要的結論:

  1. Promise若要實現鏈式調用,then一定得返回一個新的Promise
  2. 新的Promise的狀態以及value由上一個thencallback決定。

再次回到我們自己實現的then方法,很明顯它並沒有創建一個新Promise,函數沒返回值默認返回undefined,這就解釋了為啥報這個錯了。

好了,解釋完了我們得再次改造我們的MyPromise,為then提供返回Promise的操作,以及對於thencallback結果的處理:

const resolvePromise = (result, resolve, reject) => {
  // 判斷result是不是promise
  if (result instanceof MyPromise) {
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

class MyPromise {
	// ....
  then = (fulfilledFn, rejectedFn) => {
    // 我們得在每次調用then時返回一個Promise
    return new MyPromise((resolve, reject) => {
      const callbackMap = {
        [FULFILLED]: fulfilledFn,
        [REJECTED]: rejectedFn,
        // 針對異步問題,新增pending狀態時記錄並保存回調的操作
        [PENDING]: () => {
          this.fulfilledCallback.push(fulfilledFn);
          this.rejectedCallback.push(rejectedFn);
        },
      };
      // 上一個then的callback的結果將作為新Promise的值
      const result = callbackMap[this.status](this.value);
      resolvePromise(result, resolve, reject);
    });
  };
}

經過這樣修改,再次運行代碼,我們發現then鏈式調用已經成功了!!!!

我知道上面這段代碼有同學又懵了,我建議先看看上面對於then鏈式調用我們得出的兩個結論,然后我再用兩個例子來解釋這段代碼為什么要這樣寫,別着急,我會解釋的非常清楚。

對於then返回一個Promise的修改這一點大家肯定沒問題,疑惑的點應該都在新增的resolvePromise方法中。其實在前面我們解釋過了,第一個then回調返回結果(函數沒返回默認就是undefined),會作為下一個新Promise的參數,而這個返回的結果它可能是一個數字,一個字符串,也可能是一個Promise(上面的例子就是返回了一個promise作為參數),先看一個簡單的例子:

const p1 = new Promise((resolve, reject) => {
  resolve(1);
});

p1.then((res) => {
  return 520;
}).then((res) => {
  console.log(res);// 520
});

這個例子的第一個thencallback直接返回了一個數字,但奇怪的是下一個then居然能拿到這個結果,這是因為上述代碼等同於:

const p1 = new Promise((resolve, reject) => {
  resolve(1);
});

p1.then((res) => {
  return Promise.resolve(520);
}).then((res) => {
  console.log(res);// 520
});

沒錯,這也是Promise的特性之一,如果我們的then的回調返回的是一個非Promise的結果,它等同於執行Promise.resolve(),這也是為啥我們在自定義的resolvePromise中一旦判斷result不是Promise就直接執行resolve的緣故。

強化理解,來給大家看個更離譜的例子:

Promise.resolve()
  .then(() => {
    return new Error("error!!!");
  })
  .then((res) => {
    console.log("成功啦");
  })
  .catch((err) => {
    console.log("失敗啦");
  });

猜猜這段代碼最終輸出什么?輸出成功啦,因為它等同於:

Promise.resolve()
  .then(() => {
    return Promise.resolve(new Error("error!!!"));
  })
  .then((res) => {
    console.log("成功啦");
  })
  .catch((err) => {
    console.log("失敗啦");
  });

對於Promise而言,它只是一個type類型是錯誤的value而已,當然執行成功回調。有同學可能就要問了,那這個例子假設我就是想執行catch咋辦?兩種寫法:

Promise.resolve()
  .then(() => {
  	// 第一種辦法,直接reject
    return Promise.reject(new Error("error!!!"));
  	// 第二種辦法,直接拋出錯誤
  	// throw new Error('error!!!')
  })
  .then((res) => {
    console.log("成功啦");
  })
  .catch((err) => {
    console.log("失敗啦");
  });

解釋了resolvePromise中的resolve(result),再來解釋下為什么resultPromise時執行result.then(resolv,reject)就可以了。

我們已知回調的結果會作為下一個Promise的參數,那假設這個參數自身就是個Promise,對於then返回的新Promise而言,它就得等着作為參數的Promise狀態改變,在上面我們已經演過參數是Promise但不會改變狀態的例子,結果就是下一個then不會調用。

所以對於下一個新Promise而言,我就等着參數自己送到嘴里來,你狀態變不變,以及成功或者失敗那是你自己的事,因此我們通過result.then()來等待這個參數Promise的狀態變化,只要你狀態變了,比如resolve了,那是不是得執行this.resolve方法,從而將值賦予給this.value,那么等到下一次執行then時自然就能讀取對應this.value了,是不是很巧妙?

另外,result.then(resolve, reject);這一句代碼其實是如下代碼的簡寫,不信大家可以寫個小例子驗證下:

result.then((res)=> resolve(res), (err)=> reject(err));

算了,我猜測你們可能還是懶得寫例子驗證,運行下如下代碼就懂了,其實是一個意思:

// 定時器是支持傳遞參數的
setTimeout(console.log, 1000, '聽風是風')

// 等同於
setTimeout((param)=> console.log(param), 1000, '聽風是風')

那么上面的簡寫,其實也是這個意思,然后我們畫張圖總結下上面的結論:

恭喜你,模擬Promise最為繞的一部分你弄清楚了,剩下的模擬都是小魚小蝦,我們繼續。

貳 ❀ 柒 增加then不能返回Promise自己的判斷

直接看個例子,這個代碼執行報錯:

const p1 = new Promise((resolve, reject) => {
  resolve(1);
});

const p2 = p1.then((res) => {
  console.log(res);
  return p2;
});

Uncaught (in promise) TypeError: Chaining cycle detected for promise #

結合上面我們自己實現then的理解,p1.then()返回了一個Promise p2,結果p2又成p2自己需要等待的參數,說直白點就是p2等待p2的變化,自己等自己直接陷入死循環了。對於這個問題感興趣的同學可以看看segmentfault中對於這個問題的解答 關於promise then的問題

我們也來模擬這個錯誤的捕獲,直接上代碼:

const resolvePromise = (p, result, resolve, reject) => {
  // 判斷是不是自己,如果是調用reject
  if (p === result) {
    reject(new Error("Chaining cycle detected for promise #<Promise>"));
  }
  // 判斷result是不是promise
  if (result instanceof MyPromise) {
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

class MyPromise {
	// ....

  then = (fulfilledFn, rejectedFn) => {
    // 我們得在每次調用then時返回一個Promise
    const p = new MyPromise((resolve, reject) => {
      const callbackMap = {
        [FULFILLED]: fulfilledFn,
        [REJECTED]: rejectedFn,
        // 針對異步問題,新增pending狀態時記錄並保存回調的操作
        [PENDING]: () => {
          this.fulfilledCallback.push(fulfilledFn);
          this.rejectedCallback.push(rejectedFn);
        },
      };
      // 上一個then的callback的結果將作為新Promise的值
      const result = callbackMap[this.status](this.value);
      // 新增了一個p,用於判斷是不是自己
      resolvePromise(p, result, resolve, reject);
    });
    return p;
  };
}

執行上面的代碼,結果又報錯....

index.html:159 Uncaught ReferenceError: Cannot access 'p2' before initialization

錯誤說我們不能在p2初始化好之前調用它,其實看上面那個代碼本身就很奇怪,哪有在產生自己的函數的callback中使用自己的,但這就是Promise的特性之一,咱也沒辦法。

現在思路就是讓resolvePromise(p, result, resolve, reject)這一句執行晚一點,起碼要晚於新Promise的產生,咋辦?當然是用異步,比如定時器。但我們知道Promisethen是微任務,為了更好的模擬這個異步行為,這里借用一個API,名為queueMicrotask,想詳細了解的同學可以點擊鏈接跳轉MDN,這里我們直接上個簡單的例子:

queueMicrotask(() => {
  console.log("我是異步的微任務");
});
setTimeout(() => console.log("我是異步的宏任務"));
console.log("我是同步的宏任務");

看來這個API非常符合我們的預期,因為需要考慮pending狀態暫存函數的行為,我們還是額外封裝兩個成功與失敗的微任務,繼續改造:

then = (fulfilledFn, rejectedFn) => {
  // 我們得在每次調用then時返回一個Promise
  const p = new MyPromise((resolve, reject) => {
    // 封裝成功的微任務
    const fulfilledMicrotask = () => {
      // 創建一個微任務等待 promise2 完成初始化
      queueMicrotask(() => {
        // 獲取成功回調函數的執行結果
        const result = fulfilledFn(this.value);
        // 傳入 resolvePromise 集中處理
        resolvePromise(p, result, resolve, reject);
      });
    };
    // 封裝失敗的微任務
    const rejectedMicrotask = () => {
      // 創建一個微任務等待 promise2 完成初始化
      queueMicrotask(() => {
        // 調用失敗回調,並且把原因返回
        const result = rejectedFn(this.value);
        // 傳入 resolvePromise 集中處理
        resolvePromise(p, result, resolve, reject);
      });
    };
    const callbackMap = {
      [FULFILLED]: fulfilledMicrotask,
      [REJECTED]: rejectedMicrotask,
      // 針對異步問題,新增pending狀態時記錄並保存回調的操作
      [PENDING]: () => {
        this.fulfilledCallback.push(fulfilledMicrotask);
        this.rejectedCallback.push(rejectedMicrotask);
      },
    };

    callbackMap[this.status]();
  });
  return p;
};

好了,現在執行下面這段代碼來檢驗下效果:

const p1 = new MyPromise((resolve, reject) => {
  resolve(1);
});

const p2 = p1.then((res) => {
  console.log(res);
  return p2;
});

p2.then(
  () => {},
  (err) => console.log(err)
);

有同學就要說了,你這不對啊,原生Promise是直接就報錯,你這還要p2.then()才能感知報錯。咱前面就說了,這是在模擬仿寫Promise,大致達到這個效果,而且這個小節的核心目的其實是為了引出thencallback執行為什么是異步的原因

貳 ❀ 捌 添加new Promise以及then執行錯誤的捕獲

我們知道new Promise或者then回調執行報錯是,then的錯誤回調是能成功捕獲的,我們也來模擬這個過程,這個好理解一點我們就直接上代碼:

class MyPromise {
  constructor(fn) {
    try {
      // 這里的fn其實就是new Promise傳遞的函數
      fn(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }

	// ....

  then = (fulfilledFn, rejectedFn) => {
    // 我們得在每次調用then時返回一個Promise
    const p = new MyPromise((resolve, reject) => {
      // 封裝成功的微任務
      const fulfilledMicrotask = () => {
        // 創建一個微任務等待 promise2 完成初始化
        queueMicrotask(() => {
          // 添加錯誤捕獲
          try {
            // 獲取成功回調函數的執行結果
            const result = fulfilledFn(this.value);
            // 傳入 resolvePromise 集中處理
            resolvePromise(p, result, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      };
      // 封裝失敗的微任務
      const rejectedMicrotask = () => {
        // 創建一個微任務等待 promise2 完成初始化
        queueMicrotask(() => {
          // 添加錯誤捕獲
          try {
            // 調用失敗回調,並且把原因返回
            const result = rejectedFn(this.value);
            // 傳入 resolvePromise 集中處理
            resolvePromise(p, result, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      };

      // ....
    });
    return p;
  };
}

執行如下例子,效果很理想:

const p1 = new MyPromise((resolve, reject) => {
  throw new Error("new報錯啦");
});

const p2 = p1.then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log("我是錯誤回調", err);
    throw new Error("then報錯啦");
  }
);
p2.then(
  () => {},
  (err) => console.log("我是錯誤回調", err)
);

貳 ❀ 玖 實現then無callback,或者callback不是函數時的值穿透

看標題可能不明白什么意思,看個例子就懂了:

const p1 = new Promise((resolve, reject) => {
  resolve("聽風");
});
const fn = () => {};
p1.then(fn()) // 函數調用,並不是一個函數
  .then(1) // 數字
  .then('2') // 字符串
  .then() // 不傳遞
  .then((res) => console.log(res)); // 聽風

說通俗一點就是,假設then沒有回調,或者回調根本不是一個函數,那么你就當這個then不存在,但我們的MyPromise很明顯沒考慮無回調的情況,現在實現這一點:

then = (fulfilledFn, rejectedFn) => {
  // 新增回調判斷,如果沒傳遞,那我們就定義一個單純起value接力作用的函數
  fulfilledFn =
    typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
  rejectedFn =
    typeof rejectedFn === "function"
      ? rejectedFn
      : (value) => {
          throw value;
        };
  // 我們得在每次調用then時返回一個Promise
  const p = new MyPromise((resolve, reject) => {
		// ...
  });
  return p;
};

上述代碼做的事情非常簡單,檢查兩個回調是不是函數,不是函數我們就幫它定義一個只做值接力的函數,你傳遞什么我們就原封不動返回什么的函數。為啥rejectedFn要定義成(value)=>{throw value}呢?這是因為我們希望當此函數執行時能走reject路線,所以一定得拋錯,那為什么不寫成(value)=>{throw new Error(value)}這樣?因為.then().then()這種會導致new Error執行多次,結果就不對了。我們在貳 ❀ 陸小節,提到有兩種辦法可以在報錯時讓catch捕獲,一種是直接reject(),另一種就是throw一個錯誤,后面的throw影響更小一點,所以就用這種。

經過上面的修改,此時我們再執行我們無回調的例子,此時不管是成功還是失敗,都能成功執行了。

貳 ❀ 拾 實現靜態resolve與reject

創建Promise除了new Promise之外,其實還能通過Promise.resolve()靜態方法直接獲取,但目前MyPromise只提供了實例方法,所以我們需要補全靜態方法:

class MyPromise {
	// ....

  // 靜態resolve
  static resolve(value) {
    // 加入蠶食是一個promise,原封不動的返回
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      resolve(value);
    });
  }

  // 靜態reject
  static reject(value) {
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      reject(value);
    });
  }

	// ....
}

邏輯也很簡單,如果參數是一個Promise,那就原封不動返回,如果不是,我們就手動幫他創建一個Promise即可,這個特性可以通過下面這個例子驗證:

const p1 = new Promise((resolve, reject) => {
  resolve("聽風");
});
const p2 = new Promise((resolve, reject) => {
  resolve("我是一個promise");
});

p1.then(
  (res) => {
    return Promise.resolve(p2);
  },
  (err) => console.log(err)
).then(
  (res) => console.log(res), // 我是一個promise
  (err) => console.log(err)
);

可以看到假設Promise.resolve參數本身就是一個Promise時,這個方法本質上就想啥也沒做一樣,但如果參數是一個數字,它會幫你包裝成一個Promise,我們將上述代碼的new Promise改成new MyPromise,效果完全一致,說明模擬的很理想!!

OK,那么到這里,一個滿足基本功能的MyPromise就實現完畢了,但事先說明,它並未符合Promise A+規范,如果要做到一樣,我們仍需要對then方法中做一些條件判斷,這些邏輯都是規范明確告訴你應該怎么寫,沒有什么道理可言,但鑒於這段邏輯補全對於我們理解上面的題不會有額外的幫助,因此我就不做額外的改造了,下面是一份實現到現在完整的MyPromise代碼:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

const resolvePromise = (p, result, resolve, reject) => {
  if (p === result) {
    reject(new Error("Chaining cycle detected for promise #<Promise>"));
  }
  // 判斷result是不是promise
  if (result instanceof MyPromise) {
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

class MyPromise {
  constructor(fn) {
    try {
      // 這里的fn其實就是new Promise傳遞的函數
      fn(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }

  // 初始化狀態以及成功,失敗的值
  status = PENDING;
  value = null;
  // 新增記錄成功與失敗回調的參數
  fulfilledCallback = [];
  rejectedCallback = [];

  // 靜態resolve
  static resolve(value) {
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      resolve(value);
    });
  }

  // 靜態reject
  static reject(value) {
    if (value instanceof MyPromise) {
      return value;
    }
    return new MyPromise((resolve, reject) => {
      reject(value);
    });
  }

  resolve = (value) => {
    // 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
      // 新增成功回調的調用
      while (this.fulfilledCallback.length) {
        this.fulfilledCallback.shift()?.(value);
      }
    }
  };

  reject = (reason) => {
    // 當調用reject時修改狀態成rejected,同時記錄失敗的理由
    if (this.status === PENDING) {
      this.value = reason;
      this.status = REJECTED;
      // 新增失敗回調的調用
      while (this.rejectedCallback.length) {
        this.rejectedCallback.shift()?.(reason);
      }
    }
  };

  then = (fulfilledFn, rejectedFn) => {
    // 新增回調判斷,如果沒傳遞,那我們就定義一個單純起value接力作用的函數
    fulfilledFn =
      typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
    rejectedFn =
      typeof rejectedFn === "function"
        ? rejectedFn
        : (value) => {
            throw value;
          };
    // 我們得在每次調用then時返回一個Promise
    const p = new MyPromise((resolve, reject) => {
      // 封裝成功的微任務
      const fulfilledMicrotask = () => {
        // 創建一個微任務等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 獲取成功回調函數的執行結果
            const x = fulfilledFn(this.value);
            // 傳入 resolvePromise 集中處理
            resolvePromise(p, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      };
      // 封裝失敗的微任務
      const rejectedMicrotask = () => {
        // 創建一個微任務等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 調用失敗回調,並且把原因返回
            const x = rejectedFn(this.value);
            // 傳入 resolvePromise 集中處理
            resolvePromise(p, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      };
      const callbackMap = {
        [FULFILLED]: fulfilledMicrotask,
        [REJECTED]: rejectedMicrotask,
        // 針對異步問題,新增pending狀態時記錄並保存回調的操作
        [PENDING]: () => {
          this.fulfilledCallback.push(fulfilledMicrotask);
          this.rejectedCallback.push(rejectedMicrotask);
        },
      };
      callbackMap[this.status]();
    });
    return p;
  };
}

代碼看着有點多,但事實上順着思路寫下來,其實沒有什么很大的難點。

叄 ❀ 重回面試題

MyPromise實現完畢,現在讓我們回頭再看看第一道題,現在再來分析為什么這么輸出,為了方便,我將題目加在下方:

const fn = (s) => (
  new Promise((resolve, reject) => {
    if (typeof s === 'number') {
      resolve();
    } else {
      reject();
    }
  })
  .then(
    res => console.log('參數是一個number'),
    // 注意,這里沒定義失敗回調
  )
  .catch(err => console.log('參數是一個字符串'))
)
fn('1');
fn(1);

叄 ❀ 壹 第一輪執行

我們先考慮同步執行,首先我們執行fn('1'),此時執行new Promise,因為這個過程是一個同步行為,因此它會立馬調用傳遞給Promise的回調,然后走邏輯判斷,因為不是一個數字,導致執行了reject()

緊接着執行.then,前文也說了.then注冊微任務的行為是同步,但需要注意的是,.then中並未提供失敗函調,因此對於Promise底層而言,它要做的是值和狀態的穿透,這些先不管,畢竟我們還有剩余的同步任務沒走完。

於是緊接着,我們又執行了fn(1),同樣同步執行.then()注冊了成功的回調,到這里,同步任務全部執行完成。

叄 ❀ 貳 第二輪執行

由於同步代碼全部跑完了,此時肯定得按照我們注入的微任務順序,依次執行微任務,由於fn('1')這一步的.then()沒有失敗回調,默認理解為執行了值穿透的步驟,於是返回的新Promise的狀態依舊是rejected且值為undefined(因為reject沒傳值)。

緊接着,我們執行fn(1)的成功回調,於是先輸出了參數是一個number,注意,這個成功回調只有一個console,並無返回,我們默認理解為return resolve(undefined),因此返回了一個狀態是成功,但是值是undefined的新Promise

叄 ❀ 叄 第三輪執行

兩次調用的.then又返回了兩個新promise,因為狀態一開始都改變了,所以還是先走rejectedPromise,並成功觸發.catch,此時輸出參數是一個字符串,而第二個Promise是成功狀態,不能觸發.catch,到此執行結束。

為了更好理解值穿透的解釋,我們改改代碼:

const fn = (s) => {
  new Promise((resolve, reject) => {
      if (typeof s === "number") {
        resolve(1);
      } else {
        reject(2);
      }
    })
    .then(
      (res) => console.log("參數是一個number")) // 注意,這里雖然提供了函數,但是沒返回,所以理解為  return resolve(undefined)
    // 注意,這里沒傳遞失敗函數,只要callback不是一個函數,默認值穿透拿上一步的promise
    .then(
      (succ) => console.log(succ) // 這里一定輸出undefined,畢竟上一步沒返回值,默認理解成resolve(undefined)
    )
    .catch((err) => {
      console.log("參數是一個字符串");
      console.log(err); // 這里輸出2,因為上一個then又沒失敗回調,一直穿透下來
    });
};
fn("1");
fn(1);
// 參數是一個number
// undefined
// 參數是一個字符串
// 2

而假設我們有為then提供失敗回調,那么此時返回的順序就符合一開始我們對於Promise還不太了解時能夠理解的預期:

const fn = (s) => {
  new Promise((resolve, reject) => {
    if (typeof s === "number") {
      resolve();
    } else {
      reject();
    }
  })
    .then(
      (res) => console.log("參數是一個number"),
      (err) => console.log("參數是一個字符串11")
    )
    .catch((err) => {
      console.log("參數是一個字符串");
      // 看看上一個then傳遞的value是啥
      console.log(err);
    });
};
fn("1");
fn(1);

因為有提供失敗回調,這就導致.catch不會執行了。那么到這里,第一道面試題算是非常透徹的解釋完了,也多虧手寫Promise加深了對於底層原理的理解。

我們接着聊第二道題,為了方便理解,我們將這道題的Promise全部改成MyPromise,再看看輸出如何:

MyPromise.resolve()
  .then(() => {
    console.log(0);
    return MyPromise.resolve(4);
  })
  .then((res) => {
    console.log(res);
  });

MyPromise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(5);
  })
  .then(() => {
    console.log(6);
  });

// 0 1 2 4 3 5 6

使用我們實現的MyPromise,結果發現4跑到了2后面,我們可以先站在自己實現的邏輯上解釋這個現象。

我們已知.then()會返回一個Promise,且這個Promise啥時候執行以及參數都是由.then()接收的回調函數的返回結果來決定的。而在題目中MyPromise.resolve(4)這一句,其實本質上就等同於如下代碼(參照靜態resolve實現):

MyPromise.resolve()
  .then(() => {
    console.log(0);
    return new MyPromise(resolve=>resolve(4));
  })
  .then((res) => {
    console.log(res);
  });

而在then調用中最后都需要走resolvePromise,此方法會判斷參數是否是一個Promise,如果是就需要執行result.then()

不知道你腦袋里是否已經有了一種感覺,相比.then(()=>console.log(2)),前者比后者多執行了一次.then,也就是說多創建了一次微任務,這就導致4一定晚於2輸出。

但是題目2的輸出,4其實是在3之后,會不會有一種可能,官方Promisereturn Promise.resolve(4)這種行為在底層其實創建了兩次微任務,導致4延遲了2次后才輸出呢?

在查證了V8中關於Promise的源碼,直接說結論,確實是創建了兩次微任務,因為涉及到篇幅問題,若對這兩個微任務有興趣,可直接閱讀知乎問題 promise.then 中 return Promise.resolve 后,發生了什么?,有優秀答主詳細分析了源碼中兩次微任務產生的地方,只是站在我的角度,我個人覺得了解到這個結論就好,再繼續深入分析收益不成正比,所以在這我偷個懶。

肆 ❀ 總

那么到這里,一篇長達八千多字的文章也記錄完成了,本着了解兩道面試題的態度,我們嘗試手寫了一個自己的Promise,在實現過程中,就我自己而言確實又了解了不少之前從未聽過的特性,比如Promise不能返回自己,比如.then返回的Promise的執行其實依賴了.then回調函數的結果等等。另外,我會在參考中附帶一篇我覺得很不錯的Promise面試題集合,大家也可以在看完這篇文章后嘗試做做這里面的執行題,加深對於Promise的理解。

另外,實際面試中基本沒有真讓你手寫Promise A+的題,畢竟規范那么多,手寫下來難度過大,但實際面試會有讓你手寫Promise.all或者Promise.race類似的手寫題,后續我也會把這些手寫問題給補全,那么到這里本文結束。

推薦閱讀

超耐心地毯式分析,來試試這道看似簡單但暗藏玄機的Promise順序執行題

一個思路搞定三道Promise並發編程題,手摸手教你實現一個Promise限制器

強化Promise理解,從零手寫屬於自己的Promise.all與Promise.race

伍 ❀ 參考

從一道讓我失眠的 Promise 面試題開始,深入分析 Promise 實現細節

【V8源碼補充篇】從一道讓我失眠的 Promise 面試題開始,深入分析 Promise 實現細節

promise.then 中 return Promise.resolve 后,發生了什么?

[要就來45道Promise面試題一次爽到底](


免責聲明!

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



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