70行實現Promise核心原始碼

speanut發表於2020-05-05

70行實現Promise核心原始碼

前言:

​ 一直以來都是隻會呼叫Promise的API,而且調API還是呼叫axios封裝好的Promise,太丟人了!!!沒有真正的去了解過它的原理是如何實現的,自己也看過很多博主實現的Promise,但總覺得用原型鏈的OOP晦澀難懂。

個人的理解:如果帶著觀察者模式的想法來理解Promise原始碼,你就會發現Promise本身其實一種微任務的觀察者模式,一個非同步任務的完成,res/rej的狀態回撥hook => 通知所有then()訂閱的promise物件。promise只是將觀察者模式運用到微任務。讓promise物件能夠具有很高的優先順序。說到底還是一種解藕的設計模式。

promise是誕生的原因?

​ 在瞭解Promise之前,我覺得有必要去了解一下Promise誕生的原因。 直接就那上面的axios來說吧,以前沒有出現axios的時候,大家是怎麼去與後臺介面做互動的呢? 我當時是用jQuery封裝好的AJAX去做的。下面有一個例子

$.ajax({
    type: 'POST',                     //GET or POST
    url: "jquery-ajax",               
    cache: false,                     
    data: {todo:"ajaxexample1"},      
    success: functionSucceed,         
    error: functionFailed,            
    statusCode: {                     
      404: function() {
        alert("page not found");
      }
    }
});

如果是單獨的一個請求還好,但是如果得傳送兩個相互依賴的請求呢?這時候就會出現回撥地獄的問題,不能自拔。以下就是一個簡單的例子。

a(function (result1) {
  b(result1,function (result2) {
    c(result2, function (result3) {
      d(result3, function (result4) {
        e(result4, function (result5) {
          console.log(result5)
        })
      })
    })
  })
})

假如說讓你去維護一個這樣的程式碼... 害怕的兄弟萌把害怕打在評論區[doge]。上面的程式碼有什麼問題呢?

  • 巢狀呼叫,下面的任務依賴上個任務的請求結果。如果2層還是容易理順邏輯,但是一旦出現層數過多,可讀性就會變得非常差,就像一坨屎
  • 任務的不確定性。每一個任務會有成功和失敗兩種狀態,就拿上面的程式碼,假如說有5層的巢狀,就要做5次的成功、失敗的判斷函式,明顯的增加了程式碼的複雜度,不符合Unix哲學。

用Typescript實現MyPromise

問題出來了,那解決的思路也有了:

  • 消滅巢狀
  • 合併多個錯誤

設計一個物件實現上面兩個功能,使用TypeScript的OOP相比於用原型鏈來實現會更加的容易理解。在實現Promise原始碼之前,對於Promise的用法、基本定義一定要有一個全方面的認知,不然去了解Promise也艱深晦澀。可以先去看看MDN對於Promise的本質定義

定義基本的屬性和建構函式

Promise有三種狀態:pending、 resolve、reject 對應了 等待、成功、失敗,表示一個非同步任務的狀態是怎麼樣的。

enum States {
  PENDING = 'PENDING',
  RESOLVED = 'RESOLVED',
  REJECTED = 'REJECTED'
}

class MyPromise {
  private state: States = States.PENDING;
  private handlers: any[] = [];
  private value: any;
  constructor(executor: (resolve, reject) => void) {
    try {
      executor(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }
}

handlers陣列是表示當呼叫了then()方法時,向handlers新增回撥函式。比如以下的情況,handlers中就會有兩個回撥函式,等待Promise的resolve/reject設定狀態之後,呼叫handlers裡的所有回撥函式。

let promise1 = new MyPromise(test);
let promise2 = promise1
  .then(res => {  // <=== 匿名回撥函式
    console.log(res);
    return 2;
  });
let promise3 = promise1
  .then(res => { // <=== 匿名回撥函式
    setTimeout(() => {
      console.log(res + '***********************');
      return 4;
    }, 1000);
  })

value表示的一個非同步函式返回值。

executor是帶有 resolvereject 兩個引數的函式 。Promise建構函式執行時立即呼叫executor 函式, resolvereject 兩個函式作為引數傳遞給executor(executor 函式在Promise建構函式返回所建promise例項物件前被呼叫)

回到主題,我覺得先介紹then()方法是如何實現的比較合適

實現then()

then(onSuccess?, onFail?) {
    return new MyPromise((resolve, reject) => {
      return this.attachHandler({
        onSuccess: result => {
          if (!onSuccess) return resolve(result);
          try {
            return resolve(onSuccess(result));
          } catch (e) {
            return reject(e);
          }
        },
        onFail: reason => {
          if (!onFail) return reject(reason);
          try {
            return resolve(onFail(reason));
          } catch (e) {
            return reject(e);
          }
        }
      });
    });
  }

then方法的工作原理:返回一個新的Promise物件,且向原Promise物件中的handlers新增一個包含回撥函式的物件,如果Promise處於Settled狀態,那就直接執行回撥函式,否則,得等待Promise的狀態設定。

private execHandlers = () => {
    if (this.state === States.PENDING) return;
    this.handlers.forEach(handler => {
      if (this.state === States.REJECTED) {
        return handler.onFail(this.value);
      }
      return handler.onSuccess(this.value);
    });

    this.handlers = [];
  };

private attachHandler = (handler: any) => {
    this.handlers.push(handler);
    this.execHandlers();
  };

實現resolve和reject回撥函式

按照原生的Promise.then()方法的邏輯來講,原Promise的狀態會直接影響到then方法返回的Promise的狀態,因此設定狀態的resolvereject函式邏輯如下:

private resolve = value => this.setResult(value, States.RESOLVED);

private reject = value => this.setResult(value, States.REJECTED);

private setResult = (value, state: States) => {
    const set = () => {
      if (this.state !== States.PENDING) return;
      this.value = value;
      this.state = state;
      return this.execHandlers();
    };
    setTimeout(set, 0);
};

因為無法實現真正的Promise的微任務,因此只能夠通過setTimeout(fn,0),勉強來模擬實現

private resolve = value => this.setResult(value, States.RESOLVED);

private reject = value => this.setResult(value, States.REJECTED);

private setResult = (value, state: States) => {
    const set = () => {
      if (this.state !== States.PENDING) return;
      this.value = value;
      this.state = state;
      return this.execHandlers();
    };
    setTimeout(set, 0);
  };

離真正的微任務在一些特別的程式碼上還是有很大差距的,因為setTimeout是巨集任務,在execHandlers方法中通過foreach 執行本次Promise的handlers中的回撥函式時,處於同一個事件迴圈,以下的程式碼就會和真正的Promise有出入。

function test(res, rej) {
  console.log('executor');
  setTimeout(() => {
    console.log('Timer');
    res(1);
  }, 1000);
}
console.log('start');
let promise1 = new MyPromise(test); // <== 這裡替換成原生的Promise,會發現promise2狀態不同
let promise2 = promise1.then(res => {
  console.log(res);
  return 2;
});
let promise3 = promise1.then(res => {
  console.log(promise2); //原生的狀態是resolve,MyPromise的狀態是pending
});
console.log('end');

總結

就拿上面的demo來理解整個Promise幫助我們做了什麼吧!

  • 控制檯輸出'start '
  • 建立MyPromise物件並且執行test函式,引用賦值於promise1=> 輸出'executor',向延遲佇列新增等待1s的回撥函式
  • 呼叫promise1then方法 => 建立一個新的promise例項賦於給promise2,並且新的promise例項的executor執行promise1的attachHandler,將then函式中的回撥函式物件push進promise1的handlers屬性中,如果promise1已經是settled狀態,直接更加promise1的狀態來執行不同函式
  • promise3同promise2一樣的道理,這時promise1的handlers陣列中有兩個持有回撥函式的物件,這兩個Promise3和promise2都等著promise1的setResult來執行相應的回撥,因此promise3和promise此時屬於pending狀態
  • 控制檯輸出'end'
  • 等待1秒,控制檯輸出'Timer' ,呼叫Promise1的resolve函式,向微任務佇列新增setResult狀態函式,MyPromise使用settimeout模擬微任務佇列
  • setResult狀態函式根據res/rej狀態執行handlers中的所有then新增的回撥,

Promise類的catchfinally都是在then上建立的語法糖,具體大家可以更具MDN的定義來實現,還有Promise類的靜態方法,可以參考我自己GitHub的實現。

不斷的沉澱下來,總歸會理解一個東西存在的意義。理解了promise的原理之後,去理解其他的底層實現有會一個很好的基礎,瞭解了Promise底層之後,深深的感受到設計模式的強大。

如果小夥伴們覺得不錯的話,點贊支援一下嗷 鐵汁~

以下是用Typescript實現的MyPromise原始碼,不過引數並沒有用型別,所以稱作es6的class語法也不為過。

enum States {
  PENDING = 'PENDING',
  RESOLVED = 'RESOLVED',
  REJECTED = 'REJECTED'
}
export class MyPromise {
  private state: States = States.PENDING;
  private handlers: any[] = [];
  private value: any;
  constructor(callback: (resolve, reject) => void) {
    try {
      callback(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }

  private resolve = value => this.setResult(value, States.RESOLVED);
  private reject = value => this.setResult(value, States.REJECTED);

  private setResult = (value, state: States) => {
    const set = () => {
      if (this.state !== States.PENDING) return;
      this.value = value;
      this.state = state;
      return this.execHandlers();
    };
    setTimeout(set, 0);
  };

  private execHandlers = () => {
    if (this.state === States.PENDING) return;
    this.handlers.forEach(handler => {
      if (this.state === States.REJECTED) {
        return handler.onFail(this.value);
      }
      return handler.onSuccess(this.value);
    });

    this.handlers = [];
  };

  private attachHandler = (handler: any) => {
    this.handlers.push(handler);
    this.execHandlers();
  };

  then(onSuccess?, onFail?) {
    return new MyPromise((resolve, reject) => {
      return this.attachHandler({
        onSuccess: result => {
          if (!onSuccess) return resolve(result);
          try {
            return resolve(onSuccess(result));
          } catch (e) {
            return reject(e);
          }
        },
        onFail: reason => {
          if (!onFail) return reject(reason);
          try {
            return resolve(onFail(reason));
          } catch (e) {
            return reject(e);
          }
        }
      });
    });
  }
}

參考連結

https://www.freecodecamp.org/news/how-to-implement-promises-in-javascript-1ce2680a7f51/

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

 

相關文章