1. 程式人生 > 實用技巧 >Promise的原理探究及手寫Promise

Promise的原理探究及手寫Promise

前言:

不知道大家有沒有一個煩惱,就是東西太多,學習完成後就會忘記了許多,在看Promise的時候,看到了Promise的各種規範,如Promise/APromise/BPromise/D以及Promise/A的升級版Promise/A+,而es6使用了Promise/A+規範

目錄結構

參考文獻 進入正題 Promise介紹 Promise進入正題(手寫實現) 基礎框架搭建 Promise.prototype.then Promise.prototype.finally Promise.prototype.catch Promise.all Promise.race Promise.allSettled
Promise.any Promise.resolve Promise.reject Promise.try 總結

參考文獻

阮一峰 https://es6.ruanyifeng.com/#docs/promise Promise/A+:https://promisesaplus.com/ gitPromise 地址:https://github.com/YYNGUFD/-/blob/master/js/promise/Promise.js

進入正題

前言

promise 在應用開發和使用中非常廣泛,本次主要是為了深入瞭解promise的工作原理,及使用流程進行手寫自己的promise實現; 其中包含了promise的使用用法和原理實現,

Promise介紹

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。 所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

特點:

  1. 物件不受外界影響,共包含三種狀態pengding(進行中,初始狀態),fulfilled(已成功)、reject(已失敗);只有非同步操作結果,才能夠改變當前的狀態,其他的手段無法改變
  2. 一旦狀態改變,就不會在變,其中只包含兩種可能的狀態變化 pending->fulfilled和pending變為reject,只要有這兩種情況發生,狀態就會凝固,不會在發生改變了

缺點:

  1. 無法取消promise,一旦新建就會立刻執行無法中途取消,
  2. 如果不設定回撥函式,內部發生錯誤不會反應到外部;
  3. 處於pending的狀態時候,無法得到目前是哪一個階段

Promise進入正題(手寫實現)

使用方法可參照:https://es6.ruanyifeng.com/#docs/promise 本次手寫也是參照此用法,對輸入和輸出進行的控制 Promise 作為一個建構函式,其在new的時候就是立刻進行執行,根據其屬性和行為去構建基礎執行流程

基礎框架搭建

promise/A+規範

此處重點梳理 Promies/A+規範要求:
  • 當promise的狀態是pending的時候,可能會轉化到fulfilled或者rejected狀態
  • 當promise狀態是filfilled的時候
    • 不能轉化成其他的狀態
    • 必須返回一個value,並且這個value保持不變
  • 當promise的狀態是reject的時候
    • 也無法轉變成其他的狀態
    • 必須返回一個失敗的原因,

定義promise的狀態常量

const PENDING = 'pending';
const RESOLVED = 'fulfilled'; //成功
const REJECTED = 'rejected' //失敗

建立promise基礎框架

//建立Promise的基本類
class Promise {
  //看這個屬性 能夠在原型上使用 看屬性是否公用
  constructor(executor) {
    this.status = PENDING;
    //成功的值
    this.value = undefined;
    //失敗的原因
    this.reason = undefined;
 
    //回撥函式儲存器 主要解決非同步處理流程
    this.onReslovedCb = []; //成功回撥
    this.onRejectedCb = []; //失敗回撥

    //成功函式
    let resolve = (value) => {
    //只有在pending的時候才可以呼叫
      if (this.status == PENDING) {
        this.value = value;
        this.status = RESOLVED;
        this.onReslovedCb.forEach(fn => fn())
      }
    }
    //失敗函式
    let reject = (reason) => {
        //只有在pending的時候才可以呼叫
      if (this.status == PENDING) {
        this.reason = reason;
        this.status = REJECTED
        this.onRejectedCb.forEach(fn => fn())
      }
    }
    try {
      //執行器 預設會立即執行
      executor && executor(resolve, reject);
    } catch (e) {
      //執行的時候出現錯誤
      reject(e)
    }
  }
}

Promise.prototype.then

promise.then是用來接收promise例項的執行結果,then法可以接受兩個回撥函式作為引數。第一個回撥函式是Promise物件的狀態變為resolved時呼叫,第二個回撥函式是Promise物件的狀態變為rejected時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接受Promise物件傳出的值作為引數。 先看下promise/A+的規範 粗略列舉了重要的內容
promise.then(onFulfilled, onRejected)
  • onFulfilled,onRejected是then兩個引數
    • 如果onFulfilled 不是函式,將會被直接忽略
    • 同理 onRejected不是函式,也會被直接忽略
  • onFulfilled 是函式
    • 當promise的狀態是成功狀態的時候,其將會被回撥,返回的value會是第一個引數
    • 不能被進行呼叫在其他的狀態,而且只能呼叫一次
  • onRejected是函式的時候
    • 當promise狀態是失敗時候被呼叫,失敗的原因是第一個引數
    • 不能在其他的狀態下被呼叫,只能被呼叫一次
  • then方法能夠在同一個peomise上能夠被呼叫多毮次
    • 如果當前promise的狀態是fulfilled/onRejected,所有的then回撥都必須按照他們的呼叫初始順序執行
  • then方法 必須返回一個promise
實現程式碼:根據promise/A+規範
then(onfulfilled, onrejected) {
    //引數是可選則的引數 需要進行判斷是否存在
    onfulfilled = typeof onfulfilled == 'function' ? onfulfilled : data => data
    onrejected = typeof onrejected == 'function' ? onrejected : error => {
      throw error
    }
    //裡面的函式會立刻執行
    let promise2 = new Promise((resolve, reject) => {
      //成功的時候
      if (this.status == RESOLVED) { 
        //定時器處理異常 為了保障promise2已經用完了
         setTimeout(() => {
          //try 執行函式的時候會報錯 在then裡面的資料
          try {
            //x 需要判斷是否是promise和規整化  
            let x = onfulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      //失敗的時候
      if (this.status == REJECTED) {
        setTimeout(() => {
          try {
            let x = onrejected && onrejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      //如果當前是pending 表示還沒返回回來
      if (this.status == PENDING) {
        //如果是非同步 先訂閱好
        this.onReslovedCb.push(() => {
          //todo... 
          setTimeout(() => {
            try {
              let x = onfulfilled(this.value)
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.onRejectedCb.push(() => {
          //todo...

          setTimeout(() => {
            try {
              let x = onrejected(this.reason)
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }

          }, 0)
        })
      }

    })
    return promise2;
  }

then方法返回一個promise的函式,注意在進行promise2的建立的時候,我們在進行處理時候可能獲取的到的是underfined的promise2,因此需要開闢巨集任務,promise2建立完成的時候在進行呼叫,而在進行處理的時候,我們在onfulfilled、和onrejected得到的引數可能不同,他們收到的引數可能為幾種,
.then(data=>{
    return value;
},err=>{
    return value
})

value為使用者輸入,可能存在的值也是不確定的,因此需要進行判斷,而onfulfilled、onRejected呼叫後的結果也是不確定的,因此需要進行型別的判斷

//判斷then裡面的函式返回值來進行判斷  x表示當前onreject
//promise都遵循的規範,因此需要進行相容寫法
function resolvePromise(promise2, x, resolve, reject) {
  //判斷當前的x是不是promise  是不是同一個 如果是同一個 就不要等待來了 
  if (promise2 === x) {
    return reject(new TypeError("呼叫存在錯誤"))
  }
  //如果x是物件或者函式 判斷資料型別 
  /**
   * typeof 基本型別
   * constructor 
   * instanceof 判斷例項
   * Object.toString
   */
  if (typeof x === 'object' && typeof x !== null || typeof x == 'function') {
    let called; //內部測試的時候,會成功和失敗都呼叫一下 
    try {
      //取返回結果 then有可能通過defineProperty定義的
      let then = x.then
      //當前存在then方法 姑且是Promise
      if (typeof then === 'function') {
        //繫結this 到返回的x上,保證不用再次取then的值
        then.call(x, y => {
          if (called) return;
          called = true; //防止多次呼叫成功和失敗
          //y可能還是promisee  //採用promise的成功結果向下傳遞
          resolvePromise(promise2, y, resolve, reject)
        }, r => {
          if (called) return;
          called = true;
          reject(r) //採用失敗結果鄉下傳遞
        }) //保證再次取到then的值
      } else {
        //說明x就是一個普通的物件 直接成功即可
        resolve(x)
      }
    } catch (e) {
      //promise 失敗 還能進行呼叫成功
      //是一個普通的值 直接讓promise2成功即可
      if (called) return;
      called = true;
      reject(e)
    }
  } else {
    return resolve(x)
  }
}

在then方法執行完後,Promise的例項狀態就會改變成resolved、或者reject,此時then方法需要相容一非同步的呼叫型別,因此,當進入then函式後,如果當前的promise的狀態仍然是Pending,則表示當前結果還沒有返回,因此需要增加onRejectedCb、onReslovedCb用來儲存當前的執行函式,一旦某一個狀態改變,則進行呼叫該儲存列表中的資料,進行回撥;

Promise.prototype.finally

finally()方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作; finally方法的回撥函式不接受任何引數,這意味著沒有辦法知道,前面的 Promise 狀態到底是fulfilled還是rejected。這表明,finally方法裡面的操作,應該是與狀態無關的,不依賴於 Promise 的執行結果。 因此可以繫結此事件在當前promise例項的then方法上,在成功的時候回撥傳入的函式,在失敗的時候也進行回撥傳入的引數
/**
 *  finally 函式 promise m每次執行後都會進行執行
 * @param {*} cb 
 */
Promise.prototype.finally = function (cb) {
  //finally 傳入函式,無論成功或者失敗都會執行 
  return this.then(data => {
    //Promise.resolve 可以等待這個promise完成
    return Promise.resolve(cb().then(() => data))
  }, err => {
      //失敗的時候也執行
    return Promise.reject(cb().then(() => {
      throw err
    }))
  })
}

Promise.prototype.catch

Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回撥函式。
//異常處理 用於指定發生錯誤時的回撥函式。
//promise丟擲一個錯誤,就被catch()方法指定的回撥函式捕獲
Promise.prototype.catch = function (onRejected) {
  return this.then(undefined, onRejected)
}

Promise.all

Promise.all可用於接收一個數組作為引數,引數可以不是陣列,但是必須有Iterator介面,且返回的每個成員都是Promise的例項,他的結果是根據傳入的資料進行變化的 const p = Promise.all([p1, p2, p3]);
  • 只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回撥函式。
  • 只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。
/**
 * 全部成功才能成功,一個失敗才會失敗
 * promiseList 表示當前傳遞的陣列物件
 */
Promise.all = function (promiseList) {
  return new Promise((resolve, reject) => {
    let arr = [];
    let index = 0;
    //解決多個非同步併發的問題
    function proceessData(key, value) {
      arr[key] = value;
      if (++index == promiseList.length) {
        resolve(arr)
      }
    }
    for (let i = 0; i < promiseList.length; i++) {
      let current = promiseList[i];
      if (isPromise(current)) {
        current.then((data) => {
          proceessData(i, data)
        }, (err) => {
          console.log("data")
          reject(err)
        })
      } else {
        proceessData(i, current)
      }
    }
  })
}

function isPromise(value) {
  if ((typeof value === 'object' && value !== null) || typeof value === 'function') {
    if (typeof value.then == 'function') {
      return true
    }
  }
  return false;
}

Promise.race

Promise.race()方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.race([p1, p2, p3]);
上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。
/**
 * 方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
 *  @param {array} promiseList 傳遞的引數列表物件
 */
Promise.race = function (promiseList) {
  // console.log(promiseList)
  //將values中的內容包裝成promise的 
  if (!Array.isArray(promiseList)) {
    return Promise.resolve();
  }
  promiseList = promiseList.map(item => {
    return !isPromise(item) ? Promise.resolve(item) : item;
  });
  // 有一個例項率先改變狀態則進行操作   
  return new Promise((resolve, reject) => {
    promiseList.forEach((pro, index) => {
      pro.then(res => {
        resolve(res)
      }, err => {
        reject(err)
      })
    })
  })
}

Promise.allSettled

Promise.allSettled()方法接受一組 Promise 例項作為引數,包裝成一個新的 Promise 例項。只有等到所有這些引數例項都返回結果,不管是fulfilled還是rejected,包裝例項才會結束。該方法由ES2020引入。
/**
 * 方法接受一組 Promise 例項作為引數,包裝成一個新的 Promise 例項。只有等到所有這些引數例項都返回結果,
 * 不管是fulfilled還是rejected,包裝例項才會結束
 */
Promise.allSettled = function (promiseList) {
  return new Promise((resolve, reject) => {
    let index = 0;
    let arr = [] ;
    //用於記錄當前的promise的執行狀態
    function recordRequest(key, value) {
      index++;
      arr[key] = value;
      //選擇這種計數的方式,主要是考慮存在非同步的流程,等待所有流程都執行完成後在結束
      if (index == promiseList.length) {
        resolve(arr)
      }
    }
    for (let i = 0; i < promiseList.length; i++) {
      current = promiseList[i]
      if (isPromise(current)) {
        current.then((data) => {
        //每執行完成一個,就去增加記錄
          recordRequest(i, {
            status: 'resolve',
            value: data
          })
        }, (err) => {
           //失敗的promise也記錄
          recordRequest(i, {
            status: 'reject',
            reason: err
          })
        })
      } else {
        recordRequest(i, {
          status: '',
          value: current
        })
      }
    }
  })
  }

Promise.any

Promise.any()方法接受一組 Promise 例項作為引數,包裝成一個新的 Promise 例項。只要引數例項有一個變成fulfilled狀態,包裝例項就會變成fulfilled狀態如果所有引數例項都變成rejected狀態,包裝例項就會變成rejected狀態。
/**
 * Promise.any()方法接受一組 Promise 例項作為引數,包裝成一個新的 Promise 例項。只要引數例項有一個變成fulfilled狀態,包裝例項就會變成fulfilled狀態;如果所有引數例項都變成rejected狀態,包裝例項就會變成rejected狀態。該方法目前是一個第三階段的提案 。
 * @param {*} promiseList promise的引數列表
 */
Promise.any = function(promiseList){
  promiseList = promiseList.map(item => {
    return !isPromise(item) ? Promise.resolve(item) : item;
  });
  let index = 0; 
  let result=[]
  return new Promise((resolve,reject)=>{
    for (let i = 0; i < promiseList.length; i++) {
      current = promiseList[i]
      if (isPromise(current)) {
        current.then((data) => {
          resolve(data)
        }, (err) => { 
          index++; 
          result.push(err)
          if(index == promiseList.length){
            reject(err);
          } 
        })
      } 
    }
  })
}
Promise.any()丟擲的錯誤,不是一個一般的錯誤,而是一個 AggregateError 例項。它相當於一個數組,每個成員對應一個被rejected的操作所丟擲的錯誤。下面是 AggregateError 的實現示例。

Promise.resolve

有時需要將現有物件轉為 Promise 物件,Promise.resolve()方法就起到這個作用。會返回一個狀態為Resolved狀態的promise Promise.resolve(value),其中value的值包含好多種
  • 引數是一個 Promise 例項
  • 引數是一個thenable物件
  • 引數不是具有then()方法的物件,或根本就不是物件
  • 不帶有任何引數
引數的型別可能存在幾種情況
/**
 * Promis.resolve 函式
 * @param {*} values 傳遞進來的變數函式
 */
Promise.resolve = function (values) {
  //1.引數是一個 Promise 例項 將原封不動的返回
  if (values instanceof Promise) {
    return values;
  }
  return new Promise((resolve, reject) => {
    //2.引數是一個含有then物件 具有then方法
    //Promise.resolve()方法會將這個物件轉為 Promise 物件,然後就立即執行thenable物件的then()方法。
    if (isPromise(values)) {
      values.then(resolve, reject);
    } else {
      //3.引數不是具有then()方法的物件,或根本就不是物件  如果引數是一個原始值,或者是一個不具有then()方法的物件,則Promise.resolve()方法返回一個新的 Promise 物件,狀態為resolved。
      //4.引數不是具有then()方法的物件,或根本就不是物件
      //5.不帶有任何引數 
      resolve(values)
    }
  })
}

Promise.reject

Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected。
/**
 * //Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected。
 * 引數為values字串
 */
Promise.reject = function (values) {
  return new Promise((resolve, reject) => {
      reject(values)
  })
}

Promise.try

實用場景: 不知道或者不想區分,函式f是同步函式還是非同步操作,但是想用 Promise 來處理它。因為這樣就可以不管f是否包含非同步操作,都用then方法指定下一步流程,用catch方法處理f丟擲的錯誤。一般就會採用下面的寫法。 由於Promise.try為所有操作提供了統一的處理機制,所以如果想用then方法管理流程,用Promise.try包裝一下,可以更好地管理異常。
Promise.try = function (fn, argumnts = null, ...args) {
  if (typeof fn == 'function') {
  //立刻執行fn函式並進行呼叫返回
    return new Promise(resolve => resolve(fn.apply(argumnts, args)))
  } else {
    const err = new TypeError(`${typeof fn} ${fn} is not a function`);
    return Promise.try(() => {
      throw err
    });
  }
}

總結

Promise的使用方法在我們使用中還是比較多,在進行使用的時候,要進行判斷需要使用哪一個方法,掌握Promise的類方法和原型方法,其中原型方法可以進行由於返回的是promise 因此可以進行鏈式的呼叫,而直接呼叫類方法後,也可以進行鏈式的呼叫 關於幾個傳入多個函式的Promise的方法進行總結表
方法 作用
Promise.all()

1.所有的狀態都變成功狀態,才會返回成功,此時的結果將會組成一個數組,進行返回

2.其中一個為rejected的時候,p的狀態就會變成rejected,返回第一個被rejected的例項返回值

Promise.any()

1.接收一組promise的例項,

2.只要引數例項有一個變成fulfilled狀態,包裝例項就會變成fulfilled狀態;

3.如果所有引數例項都變成rejected狀態,包裝例項就會變成rejected狀態

Promise.race() 1.只要其中的一個例項先改變狀態,p的狀態就會改變,哪個率先改變Promise的返回值,就會傳給例項函式
Promise.allSettled()

1.接收多個請求

2.等待所有的函式都執行完成後才會進行返回

3.不涉及到函式返回的狀態是成功還是失敗,只要都處理完成,才返回