<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          這篇手寫 Promise 你復習前一定要康康

          共 21522字,需瀏覽 44分鐘

           ·

          2023-11-09 16:44

          大廠技術(shù)  高級前端  Node進階

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復1,加入高級Node交流群

          作者:deepfunc

          https://juejin.cn/post/7085298532365631501

          最近重溫了一下 Q/Promise[1] 的設(shè)計講解,結(jié)合自己的理解和一些小優(yōu)化,決定也來寫一篇手寫 Promise 的文章。本文的內(nèi)容適合對 Promise 的使用有一定了解的童鞋,因為過程中不會過多解釋 Promise 的基礎(chǔ)操作。我們從一個基礎(chǔ)版本開始,漸進式地完成這個 Promise,在過程中分享我的理解和觀點。內(nèi)容可能有點長,廢話不多說,我們開始吧。

          基礎(chǔ)版本

          我們先以觀察者模式作為基石來搭建一個基礎(chǔ)版本,實現(xiàn)的功能如下:

          1. 構(gòu)造函數(shù)接受一個函數(shù) exector 作為參數(shù),該函數(shù)的第一個參數(shù)是 resolve,作用是把 Promise 對象的狀態(tài)變?yōu)椤俺晒Α薄?
          2. 原型方法 then 是用來注冊一個當狀態(tài)變?yōu)槌晒Φ幕卣{(diào)函數(shù),當回調(diào)觸發(fā)時,參數(shù)是 resolve 時的決議值。
          function Promise(exector{
            this.pending = [];
            this.value = undefined;

            const resolve = value => {
              if (this.pending) {
                this.value = value;
                for (const onFulfilled of this.pending) {
                  // 通知觀察者。
                  onFulfilled(this.value);
                }
                this.pending = undefined;
              }
            };

            exector(resolve);
          }

          Promise.prototype.then = function (onFulfilled{
            if (this.pending) {
              // 還沒決議,先注冊觀察者。
              this.pending.push(onFulfilled);
            } else {
              // 已決議,直接通知。
              onFulfilled(this.value);
            }
          };

          // 測試一下。
          const p = new Promise(resolve => {
            setTimeout(() => resolve(666), 100);
          })

          p.then(res => console.log('res: %s', res));

          // 輸出:
          // res: 666

          代碼很簡單,應(yīng)該不用過多解釋,上面的完整代碼在這里:p0.js[2]

          這個基礎(chǔ)版本有個明顯的問題:then  不能進行鏈式調(diào)用,接著就來優(yōu)化一下。

          then 鏈式調(diào)用

          then 的鏈式調(diào)用會返回一個新的 Promise,并且 then 中回調(diào)的返回值會使這個新的 Promise 決議為“成功”狀態(tài)。

          Promise.prototype.then = function (onFulfilled{
            // “當前”Promise,對于返回的新 Promise 而言,也是“前一個”Promise。
            const prev = this;

            const promise = new Promise(resolve => {
              // 包裝 onFulfilled,使其可以“傳播”決議;
              // “前一個” Promise 決議后,決議返回的這個新 Promise。
              const onSpreadFulfilled = function (value{
                resolve(onFulfilled(value));
              };

              if (prev.pending) {
                prev.pending.push(onSpreadFulfilled);
              } else {
                onSpreadFulfilled(prev.value);
              }
            });

            return promise;
          };

          // 測試一下。
          const p = new Promise(resolve => {
            setTimeout(() => resolve(666), 100);
          });

          p.then(res => {
            console.log('res1: %s', res);
            return res + 1;
          ).then(res => {
            console.log('res2: %s', res);
          );
            
          // 輸出:
          // res1: 666
          // res2: 667

          實現(xiàn)鏈式調(diào)用的關(guān)鍵是如何決議返回的新 Promise?這里我對變量做了一些有含義的命名,方便理解:

          1. prev 是調(diào)用 then 時“當前”的 Promise,對于返回的新 Promise 而言,可以看做是“前一個”Promise。
          2. 包裝 onFulfilled——執(zhí)行完當前注冊的 onFulfilled 后,用其返回值來決議返回的那個新的 Promise。這是個關(guān)鍵步驟,為體現(xiàn)傳播的動作,將其命名為 onSpreadFulfilled
          3. onSpreadFulfilled 作為成功的回調(diào)注冊到 prev 上。

          上面的完整代碼在這里:p1.js[3]。

          現(xiàn)在又有個新問題,如果 resolvevalue 是個 Promise,或者 onfulfilled 函數(shù)返回的結(jié)果是個 Promise,那么鏈式傳播的決議值不應(yīng)該是這個 Promise 本身,而是這個 Promise 的決議值才對,也就是要支持 Promise 的狀態(tài)傳遞。

          狀態(tài)傳遞

          在實現(xiàn)狀態(tài)傳遞之前,我們先來康康如何確定一個值是不是 Promise。我們可以用原型繼承來判斷:

          return value instanceof Promise;

          這樣的缺點是兼容性較差,你無法強制使用者的運行環(huán)境上下文中只會用一種 Promise 的庫,或者在不同的運行上下文中傳遞 Promise 實例。所以這里我們使用 鴨子類型[4] 來判斷 Promise,重點關(guān)注對象的行為,將 Promise 看作是一個 thenable 對象。

          function isPromise(value{
            // 如果這個對象上可以調(diào)用 then 方法,就認為它是一個“Promise”了。
            return value && typeof value.then === 'function';
          }

          接下來就來實現(xiàn)狀態(tài)傳遞了,實現(xiàn)的思路就是基于鴨子類型和“通知轉(zhuǎn)移”。我們先定義一個函數(shù):

          function wrapToThenable(value{
            if (isPromise(value)) {
              return value;
            } else {
              return {
                thenfunction (onFulfilled{
                  return wrapToThenable(onFulfilled(value));
                }
              };
            }
          }

          顧名思義,這個函數(shù)的作用是用來把一個值包裝為 thenable 對象:如果 value 是 Promise 則直接返回;如果不是就包裝并返回一個有 then 方法的對象,也就是 thenable 對象。這個 thenable 對象的作用是啥呢?接著看這里:

          function Promise(exector{
            this.pending = [];
            this.value = undefined;

            const resolve = value => {
              if (this.pending) {
                // 包裝為 thenable。
                this.value = wrapToThenable(value);
                for (const onFulfilled of this.pending) {
                  // 通知時改為調(diào)用 thenable 上的 then。
                  this.value.then(onFulfilled);
                }
                this.pending = undefined;
              }
            };

            exector(resolve);
          }

          resolve 決議時,根據(jù) value 的類型不同,有兩種處理情況:

          1. 如果 value 是普通值,經(jīng)過 wrapToThenable 會包裝為 thenable 對象,通知時調(diào)用 then 方法相當于直接調(diào)用 onFulfilled
          2. 如果 value 是 Promise,則把 onFulfilled 注冊到 value 上;等到 value 決議時,就會調(diào)用 onFulfilled。還記得鏈式調(diào)用時的 onSpreadFulfilled 嗎?這里就是“通知轉(zhuǎn)移”了,把通知下一個 Promise 的責任轉(zhuǎn)移到了 value 身上。

          當然 then 也要做一點修改:

          Promise.prototype.then = function (onFulfilled{
            const prev = this;

            const promise = new Promise(resolve => {
              const onSpreadFulfilled = function (value{
                resolve(onFulfilled(value));
              };

              if (prev.pending) {
                prev.pending.push(onSpreadFulfilled);
              } else {
                // 這里也要改為調(diào)用 then。
                prev.value.then(onSpreadFulfilled);
              }
            });

            return promise;
          };

          // 測試一下。
          const p = new Promise(resolve => {
            setTimeout(() => resolve(666), 100);
          });

          p.then(res => {
            console.log('res1: %s', res);
            return new Promise(resolve => {
              setTimeout(() => resolve(777), 100);
            });
          }).then(res => {
            console.log('res2: %s', res);
          });

          // 輸出:
          // res1: 666
          // res2: 777

          這里來總結(jié)一下狀態(tài)傳遞的設(shè)計思路。包裝為 thenable 對象非常關(guān)鍵,作用是保持了與 Promise 一致的行為,也就是接口一致。這樣在 resolve 時我們不用特定去判斷這個值是不是 Promise,而可以用統(tǒng)一的處理方式來通知觀察者;并且也順便完成了“通知轉(zhuǎn)移”,如果 value 還沒有決議,則 then 會注冊為回調(diào),如果已決議則 then 會立即執(zhí)行。

          上面的完整代碼在這里:p2.js[5]。接下來,我們來完善一下 reject。

          失敗狀態(tài)

          當 Promise 決議失敗時,then 方法里面將只執(zhí)行第二個參數(shù) onRejected 對應(yīng)的回調(diào)。首先我們需要另一個包裝函數(shù):

          function wrapToRejected(value{
            return {
              thenfunction (_, onRejected{
                return wrapToThenable(onRejected(value));
              }
            };
          }

          這個函數(shù)的作用是一旦發(fā)生 reject(value) 時,我們把 value 變?yōu)榱硪环N thenable 對象,這個對象在執(zhí)行 then 時只會調(diào)用 onRejected

          然后改變一下構(gòu)造函數(shù):

          function Promise(exector{
            // pending 變?yōu)橐粋€二維數(shù)組,里面存放的元素是 [onFulfilled, onRejected]。
            this.pending = [];
            this.value = undefined;

            const resolve = value => {
              if (this.pending) {
                this.value = wrapToThenable(value);
                for (const handlers of this.pending) {
                  this.value.then.apply(this.value, handlers);
                }
                this.pending = undefined;
              }
            };

            const reject = value => {
              resolve(wrapToRejected(value));
            };

            exector(resolve, reject);
          }

          現(xiàn)在有一個比較大的變化:this.pending 變?yōu)榱硕S數(shù)組。這樣 this.value.then.apply 在執(zhí)行時會有三種情況:

          1. this.value 是成功決議轉(zhuǎn)換來的 thenable 對象,還記得 wrapToThenable 嗎?then 被執(zhí)行時只會調(diào)用 onFulfilled。
          2. this.value 是失敗決議轉(zhuǎn)換來的 thenable 對象,then 被執(zhí)行時只會調(diào)用 onRejected。
          3. this.value 是一個 Promise,決議會轉(zhuǎn)移到這個 Promise 上。

          同樣 then 方法也要做一些修改:

          Promise.prototype.then = function (onFulfilled, onRejected{
            const prev = this;
            
            // 注意這里給了 onFulfilled、onRejected 默認值。
            onFulfilled =
              onFulfilled ||
              function (value{
                return value;
              };
            onRejected =
              onRejected ||
              function (value{
                return wrapToRejected(value);
              };

            const promise = new Promise(resolve => {
              const onSpreadFulfilled = function (value{
                resolve(onFulfilled(value));
              };
              const onSpreadRejected = function (value{
                resolve(onRejected(value));
              };

              if (prev.pending) {
                prev.pending.push([onSpreadFulfilled, onSpreadRejected]);
              } else {
                prev.value.then(onSpreadFulfilled, onSpreadRejected);
              }
            });

            return promise;
          };

          // 測試一下。
          const p = new Promise((resolve, reject) => {
            setTimeout(() => reject(666), 100);
          });

          p.then(undefined, err => {
            console.log('err1: %s', err);
            return 1;
          }).then(res => {
            console.log('res1: %s', res);
          });

          // 輸出:
          // err1: 666
          // res1: 1

          我們要特別注意一下增加了 onFulfilled、onRejected 的默認值。在實際使用 then 時,可能只會專注處理成功或者失敗的回調(diào),但是我們又需要另外一種狀態(tài)要繼續(xù)傳播下去。這里可能有點不好理解,可以代入數(shù)據(jù)模擬一下。上面的完整代碼在這里:p3.js[6]。

          又到了思考總結(jié)時間,thenable 這個接口是關(guān)鍵所在。通過兩個包裝對象,分別處理成功和失敗的狀態(tài),在通知觀察者時可以保持統(tǒng)一的邏輯,這個設(shè)計是不是感覺很妙呢?

          接下來我們要處理一下調(diào)用時會產(chǎn)生異常的問題。

          異常處理

          我們先思考一下會有哪些地方會產(chǎn)生異常?第一個是構(gòu)造函數(shù)里面 exector 執(zhí)行的時候:

          function Promise(exector{
            this.pending = [];
            this.value = undefined;

            const resolve = value => {
              // ...
            };

            const reject = value => {
              resolve(wrapToRejected(value));
            };

            try {
              exector(resolve, reject);
            } catch (e) {
              // 如果有異常產(chǎn)生,狀態(tài)變?yōu)椤笆 薄?/span>
              reject(e);
            }
          }

          然后是onFulfilledonRejected 執(zhí)行的時候。當在以上兩個方法里產(chǎn)生異常時,狀態(tài)要變?yōu)槭?,并且需要把異常傳播下去?code style="font-size: 14px;background-color: rgba(27,31,35,.05);font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;word-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0 2px;color: #e96900;background: #f8f8f8;">then 的改動如下:

          Promise.prototype.then = function (onFulfilled, onRejected{
            // ...
            // 產(chǎn)生異常的時候包裝一下。
            const errHandler = returnWhenError(err => wrapToRejected(err));
            onFulfilled = errHandler(onFulfilled);
            onRejected = errHandler(onRejected);

            const promise = new Promise(resolve => {
              const onSpreadFulfilled = function (value{
                resolve(onFulfilled(value));
              };
              const onSpreadRejected = function (value{
                resolve(onRejected(value));
              };

              if (prev.pending) {
                prev.pending.push([onSpreadFulfilled, onSpreadRejected]);
              } else {
                prev.value.then(onSpreadFulfilled, onSpreadRejected);
              }
            });

            return promise;
          };

          // 封裝為一個可重用的高階函數(shù)。
          // 如果 fun 執(zhí)行失敗了,則返回 onError 的結(jié)果。
          function returnWhenError(onError{
            return fun =>
              (...args) => {
                let result;

                try {
                  result = fun(...args);
                } catch (e) {
                  result = onError(e);
                }

                return result;
              };
          }

          然后我們可以加入 catch 方法:

          Promise.prototype.catch = function (onRejected{
            // 在 then 中忽略掉“成功”狀態(tài)的回調(diào)。
            return Promise.prototype.then.call(thisundefined, onRejected);
          };

          // 測試一下。
          const p = new Promise(resolve => {
            setTimeout(() => resolve(666), 100);
          });

          p.then(res => {
            console.log('res1: %s', res);
            throw new Error('test error1');
          }).then(undefined, err => {
            console.log('err1: %s', err.message);
            throw new Error('test error2');
          }).catch(err => {
            console.log('err2: %s', err.message);
          });

          // 輸出:
          // res1: 666
          // err1: test error1
          // err2: test error2

          上面的完整代碼在這里:p4.js[7]

          到了這里,基本上 Promise 的基本功能就差不多完成了。不過還有一些不太完善的地方,我們來繼續(xù)做一些優(yōu)化。

          一些優(yōu)化

          封裝私有變量

          this.pendingthis.value 從外部是可以讀寫的,不夠安全和健壯。而我又還是想用構(gòu)造函數(shù)和原型方法,不想用閉包來封裝。我這里采用的是 WeakMap[8] 來達到目的,關(guān)鍵的修改如下:

          const refMap = new WeakMap();

          // ...

          function Promise(exector{
            // 用當前的實例引用作為 key,把想隱藏的數(shù)據(jù)放進一個對象里。
            refMap.set(this, {
              pending: [],
              valueundefined
            });

            const resolve = value => {
              // 取出封裝的數(shù)據(jù)。
              const data = refMap.get(this);

              if (data.pending) {
                data.value = wrapToThenable(value);
                for (const handlers of data.pending) {
                  data.value.then.apply(data.value, handlers);
                }
                data.pending = undefined;
              }
            };

            // ...
          }

          同樣 then 也修改一下:

          Promise.prototype.then = function (onFulfilled, onRejected{
            // ...

            const promise = new Promise(resolve => {
              const onSpreadFulfilled = function (value{
                resolve(onFulfilled(value));
              };
              const onSpreadRejected = function (value{
                resolve(onRejected(value));
              };
              // 取出封裝的數(shù)據(jù)。
              const data = refMap.get(prev);

              if (data.pending) {
                data.pending.push([onSpreadFulfilled, onSpreadRejected]);
              } else {
                data.value.then(onSpreadFulfilled, onSpreadRejected);
              }
            });

            return promise;
          };

          上面的完整代碼在這里:p5.js[9]。

          當 Promise 實例被垃圾回收時,對應(yīng)在 WeakMap 中的私有數(shù)據(jù)對象引用也會被消除,沒有內(nèi)存泄漏問題,這種方案非常適合用來封裝私有變量。

          調(diào)用順序

          目前的 Promise 在執(zhí)行時有調(diào)用順序問題,比如:

          const p = new Promise(resolve => resolve(1));

          p.then(res => {
            console.log('res1:', res);
            return res + 1;
          }).then(res => {
            console.log('res2:', res);
          });

          p.then(res => {
            console.log('res3:', res);
          });

          console.log('Hi!');

          // 目前的輸出是:
          // res1: 1
          // res2: 2
          // res3: 1
          // Hi!

          // 正確的輸出應(yīng)該是:
          // Hi!
          // res1: 1
          // res3: 1
          // res2: 2

          一個簡單的做法是利用 setTimeout 來改進:

          function Promise(exector{
            // ...
            
            const resolve = value => {
              const data = refMap.get(this);

              if (data.pending) {
                data.value = wrapToThenable(value);
                for (const handlers of data.pending) {
                  // 延遲執(zhí)行。
                  enqueue(() => {
                    data.value.then.apply(data.value, handlers);
                  });
                }
                data.pending = undefined;
              }
            };
            
            // ...
          }

          Promise.prototype.then = function (onFulfilled, onRejected{
            // ...

            const promise = new Promise(resolve => {
              // ...

              if (data.pending) {
                data.pending.push([onSpreadFulfilled, onSpreadRejected]);
              } else {
                // 延遲執(zhí)行。
                enqueue(() => {
                  data.value.then(onSpreadFulfilled, onSpreadRejected);
                });
              }
            });

            return promise;
          };

          function enqueue(callback{
            setTimeout(callback, 1);
          }

          enqueue 的作用是模擬按入隊順序來延遲執(zhí)行函數(shù)。通過對所有 then 調(diào)用的延遲執(zhí)行,可以保證按正確的注冊順序和決議順序來執(zhí)行了,上面的完整代碼在這里:p6.js[10]

          接下來呢?

          到了這里我覺得就先差不多了,畢竟此文的目的是分享和交流一種 Promise 的設(shè)計思路和心得,而不是去造一個完美的 Promise。手寫一個 Promise 這個結(jié)果不應(yīng)該是我們的目的,觀察演進過程中的思路和方案才是我們需要吸收的東西。

          向下滑動查看


          參考資料

          [1] Q/Promise: https://github.com/kriskowal/q/blob/master/design/README.md
          [2] p0.js: https://github.com/deepfunc/js-bullshit-blog/blob/master/設(shè)計模式/漸進式實現(xiàn)Promise/src/p0.js
          [3] p1.js: https://github.com/deepfunc/js-bullshit-blog/blob/master/設(shè)計模式/漸進式實現(xiàn)Promise/src/p1.js
          [4] 鴨子類型: https://zh.wikipedia.org/wiki/鴨子類型
          [5] p2.js: https://github.com/deepfunc/js-bullshit-blog/blob/master/設(shè)計模式/漸進式實現(xiàn)Promise/src/p2.js
          [6] p3.js: https://github.com/deepfunc/js-bullshit-blog/blob/master/設(shè)計模式/漸進式實現(xiàn)Promise/src/p3.js
          [7] p4.js: https://github.com/deepfunc/js-bullshit-blog/blob/master/設(shè)計模式/漸進式實現(xiàn)Promise/src/p4.js
          [8] WeakMap: https://es6.ruanyifeng.com/#docs/set-map#WeakMap
          [9] p5.js: https://github.com/deepfunc/js-bullshit-blog/blob/master/設(shè)計模式/漸進式實現(xiàn)Promise/src/p5.js
          [10] p6.js: https://github.com/deepfunc/js-bullshit-blog/blob/master/設(shè)計模式/漸進式實現(xiàn)Promise/src/p6.js

          Node 社群

               
               


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。

             “分享、點贊在看” 支持一下

          瀏覽 629
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  精品视频一二三区 | 青草视频在线免费观看 | 色五月婷婷国产后入 | 色先锋资源网 | 黄色一级片免费直播 |