Promise的原理探究及手寫Promise
阿新 • • 發佈:2020-10-26
前言:
不知道大家有沒有一個煩惱,就是東西太多,學習完成後就會忘記了許多,在看Promise的時候,看到了Promise的各種規範,如Promise/A
,Promise/B
,Promise/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參考文獻
阮一峰 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介紹
特點:
- 物件不受外界影響,共包含三種狀態pengding(進行中,初始狀態),fulfilled(已成功)、reject(已失敗);只有非同步操作結果,才能夠改變當前的狀態,其他的手段無法改變
- 一旦狀態改變,就不會在變,其中只包含兩種可能的狀態變化 pending->fulfilled和pending變為reject,只要有這兩種情況發生,狀態就會凝固,不會在發生改變了
缺點:
- 無法取消promise,一旦新建就會立刻執行無法中途取消,
- 如果不設定回撥函式,內部發生錯誤不會反應到外部;
- 處於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
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.只要引數例項有一個變成 3.如果所有引數例項都變成 |
Promise.race() | 1.只要其中的一個例項先改變狀態,p的狀態就會改變,哪個率先改變Promise的返回值,就會傳給例項函式 |
Promise.allSettled() |
1.接收多個請求 2.等待所有的函式都執行完成後才會進行返回 3.不涉及到函式返回的狀態是成功還是失敗,只要都處理完成,才返回 |