前言
[實踐系列] 主要是讓我們通過實踐去加深對一些原理的理解。
有興趣的同學可以關注 [實踐系列] 。 求star求follow~
什麼是Promise ?
Promise是JS非同步程式設計中的重要概念,非同步抽象處理物件,是目前比較流行Javascript非同步程式設計解決方案之一
Promises/A+ 規範
為實現者提供一個健全的、可互操作的 JavaScript promise 的開放標準。
術語
-
解決 (fulfill) : 指一個 promise 成功時進行的一系列操作,如狀態的改變、回撥的執行。雖然規範中用 fulfill 來表示解決,但在後世的 promise 實現多以 resolve 來指代之。
-
拒絕(reject) : 指一個 promise 失敗時進行的一系列操作。
-
拒因 (reason) : 也就是拒絕原因,指在 promise 被拒絕時傳遞給拒絕回撥的值。
-
終值(eventual value) : 所謂終值,指的是 promise 被解決時傳遞給解決回撥的值,由於 promise 有一次性的特徵,因此當這個值被傳遞時,標誌著 promise 等待態的結束,故稱之終值,有時也直接簡稱為值(value)。
-
Promise : promise 是一個擁有 then 方法的物件或函式,其行為符合本規範。
-
thenable : 是一個定義了 then 方法的物件或函式,文中譯作“擁有 then 方法”。
-
異常(exception) : 是使用 throw 語句丟擲的一個值。
基本要求
下面我們先來講述Promise/A+ 規範的幾個基本要求。
1. Promise的狀態
一個Promise的當前狀態必須是以下三種狀態中的一種: 等待狀態(Pending) 執行狀態(Fulfilled) 和 拒絕狀態(Rejected)。
const PENDING = `pending`;
const FULFILLED = `fulfilled`;
const REJECTED = `rejected`;
複製程式碼
等待狀態 (Pending)
處於等待態時,promise 需滿足以下條件:
- 可以遷移至執行態或拒絕態
if (this.state === PENDING) {
this.state = FULFILLED || REJECTED ;
}
複製程式碼
執行狀態 (Fulfilled)
處於執行態時,promise 需滿足以下條件:
-
不能遷移至其他任何狀態
-
必須擁有一個不可變的終值
this.value = value;
複製程式碼
拒絕狀態 (Rejected)
處於拒絕態時,promise 需滿足以下條件:
-
不能遷移至其他任何狀態
-
必須擁有一個不可變的據因
this.reason = reason;
複製程式碼
這裡的不可變指的是恆等(即可用 === 判斷相等),而不是意味著更深層次的不可變(譯者注:蓋指當 value 或 reason 不是基本值時,只要求其引用地址相等,但屬性值可被更改)
2. Then 方法
一個 promise 必須提供一個 then 方法以訪問其當前值、終值和據因。
promise 的 then 方法接受兩個引數:
promise.then(onFulfilled, onRejected)
複製程式碼
引數可選
onFulfilled 和 onRejected 都是可選引數。
-
如果 onFulfilled 不是函式,其必須被忽略
-
如果 onRejected 不是函式,其必須被忽略
onFulfilled 特性
如果 onFulfilled 是函式:
-
當 promise 執行結束後其必須被呼叫,其第一個引數為 promise 的終值
-
在 promise 執行結束前其不可被呼叫
-
其呼叫次數不可超過一次
onRejected 特性
如果 onRejected 是函式:
-
當 promise 被拒絕執行後其必須被呼叫,其第一個引數為 promise 的據因
-
在 promise 被拒絕執行前其不可被呼叫
-
其呼叫次數不可超過一次
呼叫時機
onFulfilled 和 onRejected 只有在執行環境堆疊僅包含平臺程式碼時才可被呼叫 注1
注1 這裡的平臺程式碼指的是引擎、環境以及 promise 的實施程式碼。實踐中要確保 onFulfilled 和 onRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。
這個事件佇列可以採用“巨集任務(macro – task)”機制或者“微任務(micro – task)”機制來實現。
由於 promise 的實施程式碼本身就是平臺程式碼(譯者注:即都是 JavaScript),故程式碼自身在處理在處理程式時可能已經包含一個任務排程佇列。
呼叫要求
onFulfilled 和 onRejected 必須被作為函式呼叫(即沒有 this 值)
多次呼叫
then 方法可以被同一個 promise 呼叫多次
-
當 promise 成功執行時,所有 onFulfilled 需按照其註冊順序依次回撥
-
當 promise 被拒絕執行時,所有的 onRejected 需按照其註冊順序依次回撥
簡易版實踐
我們先通過實踐一個簡易版的Promise來消化一下上面Promises/A+規範的基本要求。
首先
npm init
// 測試實現是否符合 promises/A+ 規範
npm install promises-aplus-tests -D
複製程式碼
package.json
{
"name": "ajpromise",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "promises-aplus-tests ./simple.js"
},
"author": "webfansplz",
"license": "MIT",
"devDependencies": {
"promises-aplus-tests": "^2.1.2"
}
}
複製程式碼
simple.js
//Promise 的三種狀態 (滿足要求 -> Promise的狀態)
const PENDING = `pending`;
const FULFILLED = `fulfilled`;
const REJECTED = `rejected`;
class AjPromise {
constructor(fn) {
//當前狀態
this.state = PENDING;
//終值
this.value = null;
//拒因
this.reason = null;
//成功態回撥佇列
this.onFulfilledCallbacks = [];
//拒絕態回撥佇列
this.onRejectedCallbacks = [];
//成功態回撥
const resolve = value => {
// 使用macro-task機制(setTimeout),確保onFulfilled非同步執行,且在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。
setTimeout(() => {
if (this.state === PENDING) {
// pending(等待態)遷移至 fulfilled(執行態),保證呼叫次數不超過一次。
this.state = FULFILLED;
// 終值
this.value = value;
this.onFulfilledCallbacks.map(cb => {
this.value = cb(this.value);
});
}
});
};
//拒絕態回撥
const reject = reason => {
// 使用macro-task機制(setTimeout),確保onRejected非同步執行,且在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。 (滿足要求 -> 呼叫時機)
setTimeout(() => {
if (this.state === PENDING) {
// pending(等待態)遷移至 fulfilled(拒絕態),保證呼叫次數不超過一次。
this.state = REJECTED;
//拒因
this.reason = reason;
this.onRejectedCallbacks.map(cb => {
this.reason = cb(this.reason);
});
}
});
};
try {
//執行promise
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
typeof onFulfilled === `function` && this.onFulfilledCallbacks.push(onFulfilled);
typeof onRejected === `function` && this.onRejectedCallbacks.push(onRejected);
// 返回this支援then 方法可以被同一個 promise 呼叫多次
return this;
}
}
複製程式碼
就這樣,一個簡單的promise就完成了.
new AjPromise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 2000);
})
.then(res => {
console.log(res);
return res + 1;
})
.then(res => {
console.log(res);
});
//output
// delay 2s..
// 2
// 3
複製程式碼
接下來,我們來看看我們的實現是否完全符合promises/A+規範~
npm run test
複製程式碼
GG,測試用例只過了一小部分,大部分飄紅~
OK,接下來,我們來繼續瞭解promises/A+ 進一步的規範要求~
進一步要求
由於接下來的要求比較抽象和難理解,所以我們將一步一步實踐來加深理解。
1. 返回
-
1.then方法必須返回一個promise物件
-
2.如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行下面的 Promise 解決過程:[[Resolve]](promise2, x)
-
3.如果 onFulfilled 或者 onRejected 丟擲一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e。
-
4.如果 onFulfilled 不是函式且 promise1 成功執行, promise2 必須成功執行並返回相同的值。
-
5.如果 onRejected 不是函式且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的據因。
-
6.不論 promise1 被 reject 還是被 resolve 時 promise2 都會被 resolve,只有出現異常時才會被 rejected。
我們通過以上要求來一步一步完善then方法
1.
// 1.首先,then方法必須返回一個promise物件
then(onFulfilled, onRejected) {
let newPromise;
return (newPromise = new AjPromise((resolve, reject) => {}));
}
複製程式碼
then(onFulfilled, onRejected) {
let newPromise;
return (newPromise = new AjPromise((resolve, reject) => {
// 2.如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行下面的 Promise 解決過程:[[Resolve]](promise2, x)
this.onFulfilledCallbacks.push(value => {
let x = onFulfilled(value);
//解決過程 resolvePromise
resolvePromise(newPromise, x);
});
this.onRejectedCallbacks.push(reason => {
let x = onRejected(reason);
//解決過程 resolvePromise
resolvePromise(newPromise, x);
});
}));
}
// 解決過程
function resolvePromise() {
//...
}
複製程式碼
then(onFulfilled, onRejected) {
let newPromise;
return (newPromise = new AjPromise((resolve, reject) => {
// 3.如果 onFulfilled 或者 onRejected 丟擲一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e。
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x);
} catch (e) {
reject(e);
}
});
}));
}
複製程式碼
4,5.
then(onFulfilled, onRejected) {
let newPromise;
// 4.如果 onFulfilled 不是函式且 promise1 成功執行, promise2 必須成功執行並返回相同的值。
onFulfilled = typeof onFulfilled === `function` ? onFulfilled : value => value;
// 5.如果 onRejected 不是函式且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的據因。
onRejected =
typeof onRejected === `function`
? onRejected
: reason => {
throw reason;
};
return (newPromise = new AjPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x);
} catch (e) {
reject(e);
}
});
}));
}
複製程式碼
then(onFulfilled, onRejected) {
let newPromise;
onFulfilled = typeof onFulfilled === `function` ? onFulfilled : value => value;
onRejected =
typeof onRejected === `function`
? onRejected
: reason => {
throw reason;
};
// 2.2.6規範 對於一個promise,它的then方法可以呼叫多次.
// 當在其他程式中多次呼叫同一個promise的then時 由於之前狀態已經為FULFILLED / REJECTED狀態,則會走以下邏輯,
// 所以要確保為FULFILLED / REJECTED狀態後 也要非同步執行onFulfilled / onRejected ,這裡使用setTimeout
// 6.不論 promise1 被 reject 還是被 resolve 時 promise2 都會被 resolve,只有出現異常時才會被 rejected。
// 由於在接下來的解決過程中需要呼叫resolve,reject進行處理,處理我們在呼叫處理過程時,傳入引數
if (this.state == FULFILLED) {
return (newPromise = new AjPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (this.state == REJECTED) {
return (newPromise = new AjPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (this.state === PENDING) {
return (newPromise = new AjPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
}
複製程式碼
ok,完整的then方法搞定了。相信通過以上實踐,你對返回要求已經有了更深的理解。
2. Promise 解決過程
Promise 解決過程是一個抽象的操作,其需輸入一個 promise 和一個值,我們表示為 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一個 Promise ,解決程式即嘗試使 promise 接受 x 的狀態;否則其用 x 的值來執行 promise 。
這種 thenable 的特性使得 Promise 的實現更具有通用性:只要其暴露出一個遵循 Promise/A+ 協議的 then 方法即可;這同時也使遵循 Promise/A+ 規範的實現可以與那些不太規範但可用的實現能良好共存。
執行 [[Resolve]](promise, x) 需遵循以下步驟:
1。x 與 promise 相等
如果 promise 和 x 指向同一物件,以 TypeError 為據因拒絕執行 promise。
2。x 為 Promise
-
如果 x 為 Promise ,則使 promise 接受 x 的狀態。
-
如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕。
-
如果 x 處於執行態,用相同的值執行 promise。
-
如果 x 處於拒絕態,用相同的據因拒絕 promise。
3。x 為物件或函式
如果 x 為物件或者函式:
-
把 x.then 賦值給 then。
-
如果取 x.then 的值時丟擲錯誤 e ,則以 e 為據因拒絕 promise。
-
如果 then 是函式,將 x 作為函式的作用域 this 呼叫之。傳遞兩個回撥函式作為引數,第一個引數叫做 resolvePromise ,第二個引數叫做 rejectPromise:
- 如果 resolvePromise 以值 y 為引數被呼叫,則執行 [[Resolve]](promise, y)
- 如果 rejectPromise 以據因 r 為引數被呼叫,則以據因 r 拒絕 promise
- 如果 resolvePromise 和 rejectPromise 均被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
- 如果呼叫 then 方法丟擲了異常 e:
- 如果 resolvePromise 或 rejectPromise 已經被呼叫,則忽略之
- 否則以 e 為據因拒絕 promise
- 如果 then 不是函式,以 x 為引數執行 promise
-
如果 x 不為物件或者函式,以 x 為引數執行 promise
如果一個 promise 被一個迴圈的 thenable 鏈中的物件解決,而 [[Resolve]](promise, thenable) 的遞迴性質又使得其被再次呼叫,根據上述的演算法將會陷入無限遞迴之中。演算法雖不強制要求,但也鼓勵施者檢測這樣的遞迴是否存在,若檢測到存在則以一個可識別的 TypeError 為據因來拒絕 promise 。
1.x 與 promise 相等
function resolvePromise(promise2, x, resolve, reject) {
//x 與 promise 相等
//如果從onFulfilled中返回的x 就是promise2 就會導致迴圈引用報錯
//如果 promise 和 x 指向同一物件,以 TypeError 為據因拒絕執行 promise
if (x === promise2) {
reject(new TypeError(`迴圈引用`));
}
}
複製程式碼
2.x 為 Promise。
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(new TypeError(`迴圈引用`));
}
// x 為 Promise
else if (x instanceof AjPromise) {
// 如果 x 為 Promise ,則使 promise 接受 x 的狀態
// 如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕
if (x.state === PENDING) {
x.then(
y => {
resolvePromise(promise2, y, resolve, reject);
},
reason => {
reject(reason);
}
);
} else {
// 如果 x 處於執行態,用相同的值執行 promise
// 如果 x 處於拒絕態,用相同的據因拒絕 promise
x.then(resolve, reject);
}
}
}
複製程式碼
3.x 為物件或函式
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(new TypeError(`迴圈引用`));
}
if (x instanceof AjPromise) {
if (x.state === PENDING) {
x.then(
y => {
resolvePromise(promise2, y, resolve, reject);
},
reason => {
reject(reason);
}
);
} else {
x.then(resolve, reject);
}
} else if (x && (typeof x === `function` || typeof x === `object`)) {
// 避免多次呼叫
let called = false;
try {
//把 x.then 賦值給 then
let then = x.then;
if (typeof then === `function`) {
// 如果 then 是函式,將 x 作為函式的作用域 this 呼叫之。
// 傳遞兩個回撥函式作為引數,第一個引數叫做 resolvePromise ,第二個引數叫做 rejectPromise
// 如果 resolvePromise 和 rejectPromise 均被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
then.call(
x,
// 如果 resolvePromise 以值 y 為引數被呼叫,則執行[[Resolve]](promise, y)
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
// 如果 rejectPromise 以據因 r 為引數被呼叫,則以據因 r 拒絕 promise
r => {
if (called) return;
called = true;
reject(r);
}
);
}else {
// 如果 then 不是函式,以 x 為引數執行 promise
resolve(x);
}
} catch (e) {
// 如果取 x.then 的值時丟擲錯誤 e ,則以 e 為據因拒絕 promise
// 如果呼叫 then 方法丟擲了異常 e:
// 如果 resolvePromise 或 rejectPromise 已經被呼叫,則忽略之
// 否則以 e 為據因拒絕 promise
if (called) return;
called = true;
reject(e);
}
} else {
// 如果 x 不為物件或者函式,以 x 為引數執行 promise
resolve(x);
}
}
複製程式碼
Ok~比較複雜的解決過程也讓我們搞定了.接下來我們整合下程式碼
Promises/A+ 規範 完整程式碼
const PENDING = `pending`;
const FULFILLED = `fulfilled`;
const REJECTED = `rejected`;
class AjPromise {
constructor(fn) {
this.state = PENDING;
this.value = null;
this.reason = null;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject);
}
setTimeout(() => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledCallbacks.map(cb => {
cb = cb(this.value);
});
}
});
};
const reject = reason => {
setTimeout(() => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.map(cb => {
cb = cb(this.reason);
});
}
});
};
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
let newPromise;
onFulfilled = typeof onFulfilled === `function` ? onFulfilled : value => value;
onRejected =
typeof onRejected === `function`
? onRejected
: reason => {
throw reason;
};
if (this.state === FULFILLED) {
return (newPromise = new AjPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (this.state === REJECTED) {
return (newPromise = new AjPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (this.state === PENDING) {
return (newPromise = new AjPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) {
reject(new TypeError(`迴圈引用`));
}
if (x instanceof AjPromise) {
if (x.state === PENDING) {
x.then(
y => {
resolvePromise(promise2, y, resolve, reject);
},
reason => {
reject(reason);
}
);
} else {
x.then(resolve, reject);
}
} else if (x && (typeof x === `function` || typeof x === `object`)) {
let called = false;
try {
let then = x.then;
if (typeof then === `function`) {
then.call(
x,
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
AjPromise.deferred = function() {
let defer = {};
defer.promise = new AjPromise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
};
module.exports = AjPromise;
複製程式碼
再來看看我們的實現是否符合Promises/A+規範
npm run test
複製程式碼
nice,測試用例全部通過!
原始碼地址
如果覺得有幫助到你,請給個star支援下作者~