Promise/A+ 規範,源碼分析
Promise是前端大廠面試的一道常考題,掌握Promise用法及其相關原理,對你的面試一定有很大幫助。這篇文章主要講解Promise源碼實現,如果你還沒有掌握Promise的功能和API,推薦你先去學習一下Promise的概念和使用API,學習知識就要腳踏實地,先把基礎搞好才能深刻理解源碼的實現。
這裏推薦阮一峯老師的文章
如果你已經掌握了Promise的基本用法,我們進行下一步
Promise/A+規範
說到Promise/A+規範,很多同學可能很不理解這是一個什麼東西,下面給出兩個地址,不瞭解的同學需要先了解一下,對我們後續理解源碼很有幫助,先看兩遍,有些地方看不懂也沒關係,後續我們可以通過源碼來回頭再理解,想把一個知識真的學會,就要反覆琢磨,從【肯定->否定->再肯定】不斷地深入理解,直到完全掌握。
Promise/A+規範英文地址
Promise/A+規範中文翻譯
如果你看過了Promise/A+規範,我們繼續,我會帶着大家按照規範要求,一步一步的來實現源碼
Promise/A+ 【2.1】
2.1Promise狀態
一個promise必須處於三種狀態之一: 請求態(pending), 完成態(fulfilled),拒絕態(rejected)
2.1.1 當promise處於請求狀態(pending)時
- 2.1.1.1 promise可以轉爲fulfilled或rejected狀態
2.1.2 當promise處於完成狀態(fulfilled)時
- 2.1.2.1 promise不能轉爲任何其他狀態
2.1.2.2 必須有一個值,且此值不能改變
2.1.3 當promise處於拒絕狀態(rejected)時
- 2.1.3.1 promise不能轉爲任何其他狀態
- 2.1.3.2 必須有一個原因(reason),且此原因不能改變
我們先找需求來完成這一部分代碼,一個簡單的小架子
// 2.1 狀態常量
const PENDING = 'penfing';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';
// Promise構造函數
function MyPromise(fn) {
const that = this;
this.state = PENDING;
this.value = null;
this.resolvedCallbacks = [];
this.rejectedCallbacks = [];
function resolve() {
if (that.state === PENDING) {
}
}
function reject() {
if (that.state === PENDING) {
}
}
}
上面這段代碼完成了Promise構造函數的初步搭建,包含:
- 三個狀態的常量聲明【請求態、完成態、拒絕態】
- this.state保管狀態、this.value保存唯一值
- resolvedCallbacks 和 rejectedCallbacks 用於保存 then 中的回調,因爲當執行完 Promise 時狀態可能還是等待中,這時候應該把 then 中的回調保存起來用於狀態改變時使用
- 給fn的回調函數 reslove、reject
- resolve、reject確保只有'pedding'狀態纔可以改變狀態
下面我們來完成resolve和reject
function resolve(value) {
if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}
function reject(value) {
if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}
- 更改this.state的狀態
- 給this.value賦值
- 遍歷回調數組並執行,傳入this.value
記下來我們需要來執行新建Promise傳入的函數體
try {
fn(resolve, reject);
} catch (e){
reject(e)
}
在執行過程中可能會遇到錯誤,需要捕獲錯誤傳給reject
Promise/A+ 【2.2】
2.2 then方法
promise必須提供then方法來存取它當前或最終的值或者原因。
promise的then方法接收兩個參數:
promise.then(onFulfilled, onRejected)
2.2.1 onFulfilled和onRejected都是可選的參數:
- 2.2.1.1 如果 onFulfilled不是函數,必須忽略
- 2.2.1.1 如果 onRejected不是函數,必須忽略
2.2.2 如果onFulfilled是函數:
- 2.2.2.1 此函數必須在promise 完成(fulfilled)後被調用,並把promise 的值作爲它的第一個參數
- 2.2.2.2 此函數在promise完成(fulfilled)之前絕對不能被調用
- 2.2.2.2 此函數絕對不能被調用超過一次
2.2.3 如果onRejected是函數:
- 2.2.3.1 此函數必須在promise rejected後被調用,並把promise 的reason作爲它的第一個參數
- 2.2.3.2 此函數在promise rejected之前絕對不能被調用
- 2.2.3.2 此函數絕對不能被調用超過一次
現根據這些要求我們先實現個簡單的then函數:
MyPromise.prototype.then = function (onFulfilled, onRejected) {
const that = this
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected =
typeof onRejected === 'function'
? onRejected
: r => {
throw r
}
if (that.state === PENDING) {
that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
}
if (that.state === RESOLVED) {
onFulfilled(that.value)
}
if (that.state === REJECTED) {
onRejected(that.value)
}
}
- 首先判斷了傳進來的onFulfilled和onRejected是不是一個函數類型,如果不是就創建一個透傳數據的函數
- 判斷狀態,如果是'pending'就把函數追加到對應的隊列中,如果不是'pending',直接執行對應狀態的函數【resolves => onFulfilled, rejected => onRejected】
如上我們就完成了一個簡易版的promise,但是還不能完全滿足Promise/A+規範,接下來我們繼續完善
2.2.4 在執行上下文堆棧(execution context)僅包含平臺代碼之前,不得調用 onFulfilled和onRejected
2.2.5 onFulfilled and onRejected must be called as functions (i.e. with no this value)
2.2.6 then可以在同一個promise裏被多次調用
— 2.2.6.1 如果/當 promise 完成執行(fulfilled),各個相應的onFulfilled回調 必須根據最原始的then 順序來調用
— 2.2.6.2 如果/當 promise 被拒絕(rejected),各個相應的onRejected回調 必須根據最原始的then 順序來調用
2.2.7 then必須返回一個promise
promise2 = promise1.then(onFulfilled, onRejected);
- 2.2.7.1 如果onFulfilled或onRejected返回一個值x, 運行 Promise Resolution Procedure [[Resolve]](promise2, x)
- 2.2.7.2 如果onFulfilled或onRejected拋出一個異常e,promise2 必須被拒絕(rejected)並把e當作原因
- 2.2.7.3 如果onFulfilled不是一個方法,並且promise1已經完成(fulfilled), promise2必須使用與promise1相同的值來完成(fulfiled)
- 2.2.7.4 如果onRejected不是一個方法,並且promise1已經被拒絕(rejected), promise2必須使用與promise1相同的原因來拒絕(rejected)
首先我們先把resolve和rejected完善一下
function resolve(value) {
if (value instanceof MyPromise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}, 0)
}
function reject(value) {
setTimeout(() => {
if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}, 0)
}
參考2.2.2和2.2.3
- 對於 resolve 函數來說,首先需要判斷傳入的值是否爲 Promise 類型
- 爲了保證函數執行順序,需要將兩個函數體代碼使用 setTimeout 包裹起來
接下來根據規範需求繼續完善then函數裏的代碼:
if (that.state === PENDING) {
return (promise2 = new MyPromise((resolve, reject) => {
that.resolvedCallbacks.push(() => {
try {
const x = onFulfilled(that.value);
resoluteProcedure(promise2, x, resolve, reject)
} catch (r) {
reject(r);
}
});
that.rejectedCallbacks.push(() => {
try {
const x = onRejected(that.value);
resoluteProcedure(promise2, x, resolve, reject)
} catch {
reject(r)
}
})
}));
that.reolvedCallbacks.push(onFulfilled);
that.rejectedCallbacks.push(onRejeted);
}
if (that.state === RESOLVED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
const x = onFulfilled(that.value)
resolutionProcedure(promise2, x, resolve, reject)
} catch (reason) {
reject(reason)
}
})
}))
}
if (that.state === REJECTED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
const x = onRejected(that.value)
resolutionProcedure(promise2, x, resolve, reject)
} catch (reason) {
reject(reason)
}
})
}))
}
- 首先我們返回了一個新的 Promise 對象,並在 Promise 中傳入了一個函數
- 函數的基本邏輯還是和之前一樣,往回調數組中 push 函數
- 同樣,在執行函數的過程中可能會遇到錯誤,所以使用了 try...catch 包裹
- 規範規定,執行 onFulfilled 或者 onRejected 函數時會返回一個 x,並且執行 Promise 解決過程,這是爲了不同的 Promise 都可以兼容使用,比如 JQuery 的 Promise 能兼容 ES6 的 Promise
Promise/A+ 【2.3】
2.3 Promise解決程序
2.3.1 如果promise和x引用同一個對象,則用TypeError作爲原因拒絕(reject)promise。
2.3.2 如果x是一個promise,採用promise的狀態
- 2.3.2.1 如果x是請求狀態(pending),promise必須保持pending直到xfulfilled或rejected
- 2.3.2.2 如果x是完成態(fulfilled),用相同的值完成fulfillpromise
- 2.3.2.2 如果x是拒絕態(rejected),用相同的原因rejectpromise
2.3.3另外,如果x是個對象或者方法
- 2.3.3.1 讓x作爲x.then
- 2.3.3.2 如果取回的x.then屬性的結果爲一個異常e,用e作爲原因reject promise
-
2.3.3.3 如果then是一個方法,把x當作this來調用它, 第一個參數爲 resolvePromise,第二個參數爲rejectPromise,其中:
- 2.3.3.3.1 如果/當 resolvePromise被一個值y調用,運行 [[Resolve]](promise, y)
- 2.3.3.3.2 如果/當 rejectPromise被一個原因r調用,用r拒絕(reject)promise
- 2.3.3.3.3 如果resolvePromise和 rejectPromise都被調用,或者對同一個參數進行多次調用,第一次調用執行,任何進一步的調用都被忽略
-
2.3.3.3.4 如果調用then拋出一個異常e,
- 2.3.3.3.4.1 如果resolvePromise或 rejectPromise已被調用,忽略。
- 2.3.3.3.4.2 或者, 用e作爲reason拒絕(reject)promise
- 2.3.3.4 如果then不是一個函數,用x完成(fulfill)promise
2.3.4 如果 x既不是對象也不是函數,用x完成(fulfill)promise
如果一個promise被一個thenable resolve,並且這個thenable參與了循環的thenable環,
[[Resolve]](promise, thenable)的遞歸特性最終會引起[[Resolve]](promise, thenable)再次被調用。
遵循上述算法會導致無限遞歸,鼓勵(但不是必須)實現檢測這種遞歸併用包含信息的TypeError作爲reason拒絕(reject)
這部分規範主要描述了resolutionProcedure函數的規範,下面我們來實現resolutionProcedure這個函數,我先我麼你關注2.3.4下面那段話,簡單的來說規定了x不能與promise2相等,這樣會發生循環引用的問題,如下栗子:
let p = new MyPromise((resolve, reject) => {
resolve(1)
})
let p1 = p.then(value => {
return p1
})
所以我們需要先進行檢測,代碼如下:
function resolutionProcedure(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Error'))
}
}
接下來我們判斷x的類型
if (x instanceof MyPromise) {
x.then(function (value) {
resolutionProcedure(promise2, value, resolve, reject)
}, reject)
}
如果 x 爲 Promise 的話,需要判斷以下幾個情況:
- 如果 x 處於等待態,Promise 需保持爲等待態直至 x 被執行或拒絕
- 如果 x 處於其他狀態,則用相同的值處理 Promise
最後我們來完成剩餘的代碼:
let called = false
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return
called = true
resolutionProcedure(promise2, y, resolve, reject)
},
e => {
if (called) return
called = true
reject(e)
}
)
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x)
}
- 首先創建一個變量 called 用於判斷是否已經調用過函數
- 然後判斷 x 是否爲對象或者函數,如果都不是的話,將 x 傳入 resolve 中
- 如果 x 是對象或者函數的話,先把 x.then 賦值給 then,然後判斷 then 的類型,如果不是函數類型的話,就將 x 傳入 resolve 中
- 如果 then 是函數類型的話,就將 x 作爲函數的作用域 this 調用之,並且傳遞兩個回調函數作爲參數,第一個參數叫做 resolvePromise ,第二個參數叫做 rejectPromise,兩個回調函數都需要判斷是否已經執行過函數,然後進行相應的邏輯
- 以上代碼在執行的過程中如果拋錯了,將錯誤傳入 reject 函數中
測試Promise
有專門的測試腳本可以測試所編寫的代碼是否符合PromiseA+的規範
首先,在promise實現的代碼中,增加以下代碼:
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
安裝測試腳本:
npm install -g promises-aplus-tests
如果當前的promise源碼的文件名爲promise.js
那麼在對應的目錄執行以下命令:
promises-aplus-tests promise.js
共有872條測試用例,可以完美通過
符合Promise/A+規範完整代碼
這樣我們就完成了符合Promise/A+規範的源碼,下面是整個代碼:
const PENDING = 'pending';
const RESOLVED = 'resolve';
const REJECTED = 'rejected';
function Promise(fn) {
let that = this;
that.status = 'PENDING';
that.value = undefined;
that.resolvedCallbacks = [];
that.rejectedCallbacks = [];
function resolve(value) {
if (that.status === 'PENDING') {
that.status = 'RESOLVED';
that.value = value;
that.resolvedCallbacks.forEach(function (fn) {
fn();
})
}
}
function reject(value) {
if (that.status === 'PENDING') {
that.status = 'REJECTED';
that.value = value;
that.rejectedCallbacks.forEach(function (fn) {
fn();
})
}
}
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
function resolutionProcedure(promise2, x, resolve, reject) {
//有可能這裏返回的x是別人的promise 要儘可能允許其他人亂寫
if (promise2 === x) {//這裏應該報一個循環引用的類型錯誤
return reject(new TypeError('循環引用'));
}
//看x是不是一個promise promise應該是一個對象
let called; //表示是否調用過成功或者失敗
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
//可能是promise 看這個對象中是否有then 如果有 姑且作爲promise 用try catch防止報錯
try {
let then = x.then;
if (typeof then === 'function') {
//成功
then.call(x, function (y) {
if (called) return //避免別人寫的promise中既走resolve又走reject的情況
called = true;
resolutionProcedure(promise2, y, resolve, reject)
}, function (err) {
if (called) return
called = true;
reject(err);
})
} else {
resolve(x) //如果then不是函數 則把x作爲返回值.
}
} catch (e) {
if (called) return
called = true;
reject(e)
}
} else { //普通值
return resolve(x)
}
}
Promise.prototype.then = function (onFulfilled, onRejected) {
//成功和失敗默認不傳給一個函數
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
return value;
}
onRejected = typeof onRejected === 'function' ? onRejected : function (err) {
throw err;
}
let that = this;
let promise2; //新增: 返回的promise
if (that.status === 'RESOLVED') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () { //用setTimeOut實現異步
try {
let x = onFulfilled(that.value); //x可能是普通值 也可能是一個promise, 還可能是別人的promise
resolutionProcedure(promise2, x, resolve, reject) //寫一個方法統一處理
} catch (e) {
reject(e);
}
})
})
}
if (that.status === 'REJECTED') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onRejected(that.value);
resolutionProcedure(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
})
})
}
if (that.status === 'PENDING') {
promise2 = new Promise(function (resolve, reject) {
that.resolvedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onFulfilled(that.value);
resolutionProcedure(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
})
});
that.rejectedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onRejected(that.value);
resolutionProcedure(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
})
});
})
}
return promise2;
}
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = Promise;
總結
以上就是符合Promise/A+規範的源碼,ES6的Promise其實並不是向我們這樣通過js來實現,而是在底層實現,並且還擴展了很多新的方法:
- Promise.prototype.catch()
- Promise.prototype.finally()
- Promise.all()
- Promise.race()
- Promise.allSettled()
- Promise.any()
- Promise.resolve()
- Promise.reject()
- Promise.try()
這裏就不一一介紹啦,大家可以參考阮一峯老師的文章 ES6入門-Promise對象
這篇文章給大家講解的Promise/A+規範的源碼,希望大家能多讀多寫,深刻的體會一下源碼的思想,對以後的開發也很有幫助。
感謝大家的閱讀,覺得還不錯,辛苦點一下關注,謝謝!