首頁 > 軟體

詳解JavaScript如何實現一個簡易的Promise物件

2022-12-01 14:02:22

前言

實現一個簡易的Promise物件,我們首先要了解幾個相關的知識點:

Promise物件的狀態: pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。

Promise的引數: Promise建構函式接收一個函數作為引數,函數內部有兩個引數,分別是resolve和reject,這兩個引數是兩個函數,由JS引擎提供,不需要我們部署。reslove函數的作用是將Promise物件的狀態由 'pending' 狀態變為 'resolved'狀態即('fulfilled'狀態),方便與引數名對應,reject函數的作用是將Promise物件的狀態由 'pending' 狀態變為 'rejected'狀態。

但是我們應該注意的是,Promise物件的狀態一經改變,就不再發生改變(即pending --> resolved || pending --> rejected 其中任意一種發生改變之後,Promise物件的狀態將不再發生改變)

Promise的基礎結構與用法

 let p1 = new Promise((resolve, reject) => {
   resolve('成功');
   reject('失敗');
   throw('報錯');  //相當於reject()
 })

 console.log(p1);

讓我們看看三種狀態的列印的結果分別是什麼吧

使用class類實現promise物件

class myPromise {
  constructor(executor) {
    this.status = 'pending'; // 變更promise的狀態
    this.value = null;
    
    executor(this.resolve, this.reject); // new 一個myPromise 得到的範例物件裡面有兩個函數
  }

  resolve(value) {
    if (this.status !== 'pending') return;
    this.status = 'fulfilled'; // 變更promise的狀態
    this.value = value;
  }

  reject(reason) {
    if (this.status !== 'pending') return
    this.status = 'rejected';
    this.value = reason;
  }
}

貌似這麼寫程式碼邏輯也說得通,那麼讓我們看一下實現的效果:

看到這裡,不難想到我們的resolve和reject的兩個方法的this指向出現了問題,我們仔細看看不難發現,這兩個方法被我們作為實參放到了構造器函數,此時this的指向是指向了構造器函數,而不是我們寫的myPromise這個建構函式身上,那隻需要bind顯示繫結一下this 的指向就可以解決了。

executor(this.resolve.bind(this), this.reject.bind(this))

並且myPromise的狀態一經變更也不再改變,是不是有一點原裝Promise的味道了。但是在Promise裡面還有一個錯誤捕捉機制,只要promise裡面執行的邏輯報錯了,就需要走reject邏輯,將錯誤丟擲來,那我們只需要使用try catch來實現就可以。

try {
      executor(this.resolve.bind(this), this.reject.bind(this));
    } catch (error) {
      this.reject(error)
    }

這樣我們就實現了極簡版的Promise物件,但是通常情況下我們都是使用Promise物件來處理非同步的問題,說到非同步,那不得不提起Promise.prototype.then()這個方法了,then方法返回的是一個新的Promise範例(注意,不是原來那個Promise範例)。因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。

then方法的第一個引數是`resolved狀態的回撥函數`,第二個引數是`rejected狀態的回撥函數`,它們都是可選的。

當promise的狀態為'fulfilled'會執行第一個回撥函數,當狀態為'rejected'時執行第二個回撥函數。

必須等到Promise的狀態變更過一次之後,狀態為'fulfilled'或者'rejected',才去執行then裡面的邏輯。

.then支援鏈式呼叫,下一次.then受上一次.then執行結果的影響。

知道以上這幾點,我們就可以嘗試如何實現.then方法了

class myPromise {
  constructor(executor) {
    this.status = 'pending'; 
    this.value = null;
    this.onFulfilledCallbacks = []; // 用來儲存成功的回撥(處理非同步)
    this.onRejectedCallbacks = []; // 用來儲存失敗的回撥(處理非同步)
    try {
      executor(this.resolve.bind(this), this.reject.bind(this));
    } catch (error) {
      this.reject(error)
    }
  }

  resolve(value) {
    if (this.status !== 'pending') return;
    this.status = 'fulfilled';
    this.value = value;
    // 呼叫then裡面的回撥
    while (this.onFulfilledCallbacks.length) { // 當非同步成功回撥陣列中存在回撥函數,那就執行
      this.onFulfilledCallbacks.shift()(this.value)
    }
  }

  reject(reason) {
    if (this.status !== 'pending') return
    this.status = 'rejected';
    this.value = reason;
    while (this.onRejectedCallbacks.length) { // 當非同步失敗回撥陣列中存在回撥函數,那就執行
      this.onRejectedCallbacks.shift()(this.value)
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val  // 判斷.then的第一個引數是不是一個函數,如果不是就直接作為結果返回
    onRejected = typeof onRejected === 'function' ? onRejected : val => { throw val } // 判斷.then的第二個引數是不是一個函數,如果不是就直接作為錯誤返回

    var thenPromise = new myPromise((resolve, reject) => {  // 因為.then返回的是一個心的Promise物件

      const resolvePromise = callback => {  // 用於判斷回撥函數的型別
        setTimeout(() => {   // 讓整個回撥函數比同步程式碼晚一點執行,官方不是使用setTimeout實現
          try {
            const x = callback(this.value);
            if (x === thenPromise) {  // 你正在返回自身
              throw new Error('不允許返回自身!');
            }
            if (x instanceof myPromise) { // 返回的是一個Promise物件
              x.then(resolve, reject);
            } else { // 直接返回一個值,作為resolve的值,傳遞給下一個.then
              resolve(x);
            }
          } catch (error) {
            reject(error);
            throw new Error(error)
          }
        })
      }

      if (this.status === 'fulfilled') {
        resolvePromise(onFulfilled)
      } else if (this.status === 'rejected') {
        resolvePromise(onRejected)
      } else if (this.status === 'pending') {
        this.onFulfilledCallbacks.push(resolvePromise.bind(this, onFulfilled));
        this.onRejectedCallbacks.push(resolvePromise.bind(this, onRejected));
      }
    })
    return thenPromise
  }
}

寫在最後

最後和大家分享一下,我是如何一步一步實現簡易版的Promise

首先從Promise建構函式的特點,三種狀態,狀態一經改變就不再變化,所以在resolvereject的方法裡面加上判斷,如果不是'pending'狀態,則直接return,這樣就實現了狀態一經發生改變則不再變化,因為.then裡面回撥的執行,是根據Promise的狀態來執行,當狀態為'fulfilled'時才執行.then第一個回撥函數,裝狀態為'rejected'執行.then第二個回撥函數,但是如果在Promise裡面,在resolve或者reject的外面套上setTimeout,那麼狀態變更會加入到下一次宏任務佇列裡,那我們九就維護出兩個陣列,用來存放未執行的回撥,當狀態改變之後,在對應的resolvereject方法裡去判斷我們維護的未執行的回撥函數的陣列裡是否有未執行的回撥,如果有直接呼叫掉,並且因為.then返回的是一個Promise物件,所以我們不能直接把'onFulfilled',或者'onRejected'其中一個回撥給返回出去,否則.then後面就不能再接.then,所以在then方法裡面我們定義了一個resolvePromise函數,其目的就是在返回的'onFulfilled',或者'onRejected'外面套一層Promise物件,使得他後面能繼續接.then的回撥,在這個resolvePromise函數內部我們還新增了判斷回撥的型別,在官方的定義的Promise物件中,規定了回撥不能是原Promise物件,另外兩個判斷是回撥是一個Promise物件,以及如果不是Promise物件,那就直接resolve()出去

最後同步程式碼會優先於.then的執行,因為.then是非同步程式碼中的微任務,只有宏任務執行完之後,微任務才會執行,所以在resolvePromise的回撥外面套一層setTimeout,這樣返回出去的.then的邏輯,會去到下一次的宏任務佇列,這樣就實現了.then的執行會比同步程式碼稍晚一些,但是官方並不是使用setTimeout實現的。

到此這篇關於詳解JavaScript如何實現一個簡易的Promise物件的文章就介紹到這了,更多相關JavaScript實現Promise物件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com