本文是一塊兒學習造輪子系列的第一篇,本篇咱們將從零開始寫一個符合Promises/A+規範的promise,本系列文章將會選取一些前端比較經典的輪子進行源碼分析,而且從零開始逐步實現,本系列將會學習Promises/A+,Redux,react-redux,vue,dom-diff,webpack,babel,kao,express,async/await,jquery,Lodash,requirejs,lib-flexible等前端經典輪子的實現方式,每一章源碼都託管在github上,歡迎關注~
相關係列文章:
一塊兒學習造輪子(一):從零開始寫一個符合Promises/A+規範的promise
一塊兒學習造輪子(二):從零開始寫一個Redux
一塊兒學習造輪子(三):從零開始寫一個React-Redux
本系列github倉庫:
一塊兒學習造輪子系列github(歡迎star~)前端
Promise 是異步編程的一種解決方案,比傳統的解決方案回調函數和事件更合理更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。本篇不注重講解promise的用法,關於用法,能夠看阮一峯老師的ECMAScript 6系列裏面的Promise部分:
vue
本篇主要講解如何從零開始一步步的實現promise各項特性及功能,最終使其符合Promises/A+規範,由於講解較細,因此文章略長。 另外,每一步的項目源碼都在github上,能夠對照參考,每一步都有對應的項目代碼及測試代碼,喜歡的話,歡迎給個star~
react
項目地址:本文代碼的github倉庫jquery
本文promise裏用到的異步操做的示例都是使用的node裏面的fs.readFile方法,在瀏覽器端可使用setTimeout方法進行模擬異步操做。
webpack
function MyPromise(fn) {
let self = this; // 緩存當前promise實例
self.value = null; //成功時的值
self.error = null; //失敗時的緣由
self.onFulfilled = null; //成功的回調函數
self.onRejected = null; //失敗的回調函數
function resolve(value) {
self.value = value;
self.onFulfilled(self.value);//resolve時執行成功回調
}
function reject(error) {
self.error = error;
self.onRejected(self.error)//reject時執行失敗回調
}
fn(resolve, reject);
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
//在這裏給promise實例註冊成功和失敗回調
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
}
module.exports = MyPromise
複製代碼
代碼很短,邏輯也很是清晰,在then中註冊了這個promise實例的成功回調和失敗回調,當promise reslove時,就把異步執行結果賦值給promise實例的value,並把這個值傳入成功回調中執行,失敗就把異步執行失敗緣由賦值給promise實例的error,並把這個值傳入失敗回調並執行。
git
基礎版本代碼
es6
咱們知道,咱們在使用es6 的promise時,能夠傳入一個異步任務,也能夠傳入一個同步任務,可是咱們的上面基礎版代碼並不支持同步任務,若是咱們這樣寫就會報錯:github
let promise = new Promise((resolve, reject) => {
resolve("同步任務執行")
});
複製代碼
爲何呢?由於是同步任務,因此當咱們的promise實例reslove時,它的then方法還沒執行到,因此回調函數還沒註冊上,這時reslove中調用成功回調確定會報錯的。
web
使promise支持同步方法
function resolve(value) {
//利用setTimeout特性將具體執行放到then以後
setTimeout(() => {
self.value = value;
self.onFulfilled(self.value)
})
}
function reject(error) {
setTimeout(() => {
self.error = error;
self.onRejected(self.error)
})
}
複製代碼
實現很簡單,就是在reslove和reject裏面用setTimeout進行包裹,使其到then方法執行以後再去執行,這樣咱們就讓promise支持傳入同步方法,另外,關於這一點,Promise/A+規範裏也明確要求了這一點。
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
咱們知道在使用promise時,promise有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。另外,promise一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,若是改變已經發生了,你再對promise對象添加回調函數,也會當即獲得這個結果。
//定義三種狀態
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function MyPromise(fn) {
let self = this;
self.value = null;
self.error = null;
self.status = PENDING;
self.onFulfilled = null;
self.onRejected = null;
function resolve(value) {
//若是狀態是pending纔去修改狀態爲fulfilled並執行成功邏輯
if (self.status === PENDING) {
setTimeout(function() {
self.status = FULFILLED;
self.value = value;
self.onFulfilled(self.value);
})
}
}
function reject(error) {
//若是狀態是pending纔去修改狀態爲rejected並執行失敗邏輯
if (self.status === PENDING) {
setTimeout(function() {
self.status = REJECTED;
self.error = error;
self.onRejected(self.error);
})
}
}
fn(resolve, reject);
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
if (this.status === PENDING) {
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
} else if (this.status === FULFILLED) {
//若是狀態是fulfilled,直接執行成功回調,並將成功值傳入
onFulfilled(this.value)
} else {
//若是狀態是rejected,直接執行失敗回調,並將失敗緣由傳入
onRejected(this.error)
}
return this;
}
module.exports = MyPromise
複製代碼
首先,咱們創建了三種狀態"pending","fulfilled","rejected",而後咱們在reslove和reject中作判斷,只有狀態是pending時,纔去改變promise的狀態,並執行相應操做,另外,咱們在then中判斷,若是這個promise已經變爲"fulfilled"或"rejected"就馬上執行它的回調,並把結果傳入。
咱們平時寫promise通常都是對應的一組流程化的操做,如這樣:
promise.then(f1).then(f2).then(f3)
可是咱們以前的版本最多隻能註冊一個回調,這一節咱們就來實現鏈式操做。
使promise支持鏈式操做
想支持鏈式操做,其實很簡單,首先存儲回調時要改成使用數組
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
複製代碼
固然執行回調時,也要改爲遍歷回調數組執行回調函數
self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
複製代碼
最後,then方法也要改一下,只須要在最後一行加一個return this便可,這其實和jQuery鏈式操做的原理一致,每次調用完方法都返回自身實例,後面的方法也是實例的方法,因此能夠繼續執行。
MyPromise.prototype.then = function(onFulfilled, onRejected) {
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
} else if (this.status === FULFILLED) {
onFulfilled(this.value)
} else {
onRejected(this.error)
}
return this;
}
複製代碼
咱們上一節實現了鏈式調用,可是目前then方法裏只能傳入同步任務,可是咱們日常用promise,then方法裏通常是異步任務,由於咱們用promise主要用來解決一組流程化的異步操做,以下面這樣的調取接口獲取用戶id後,再根據用戶id調取接口獲取用戶餘額,獲取用戶id和獲取用戶餘額都須要調用接口,因此都是異步任務,如何使promise支持串行異步操做呢?
getUserId()
.then(getUserBalanceById)
.then(function (balance) {
// do sth
}, function (error) {
console.log(error);
});
複製代碼
使promise支持串行異步操做
這裏爲方便講解咱們引入一個常見場景:用promise順序讀取文件內容,場景代碼以下:
let p = new Promise((resolve, reject) => {
fs.readFile('../file/1.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
let f1 = function(data) {
console.log(data)
return new Promise((resolve, reject) => {
fs.readFile('../file/2.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
}
let f2 = function(data) {
console.log(data)
return new Promise((resolve, reject) => {
fs.readFile('../file/3.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
}
let f3 = function(data) {
console.log(data);
}
let errorLog = function(error) {
console.log(error)
}
p.then(f1).then(f2).then(f3).catch(errorLog)
//會依次輸出
//this is 1.txt
//this is 2.txt
//this is 3.txt
複製代碼
上面場景,咱們讀取完1.txt後並打印1.txt內容,再去讀取2.txt並打印2.txt內容,再去讀取3.txt並打印3.txt內容,而讀取文件都是異步操做,因此都是返回一個promise,咱們上一節實現的promise能夠實現執行完異步操做後執行後續回調,可是本節的回調讀取文件內容操做並非同步的,而是異步的,因此當讀取完1.txt後,執行它回調onFulfilledCallbacks裏面的f1,f2,f3時,異步操做尚未完成,因此咱們本想獲得這樣的輸出:
this is 1.txt
this is 2.txt
this is 3.txt
複製代碼
可是實際上卻會輸出
this is 1.txt
this is 1.txt
this is 1.txt
複製代碼
因此要想實現異步操做串行,咱們不能將回調函數都註冊在初始promise的onFulfilledCallbacks裏面,而要將每一個回調函數註冊在對應的異步操做promise的onFulfilledCallbacks裏面,用讀取文件的場景來舉例,f1要在p的onFulfilledCallbacks裏面,而f2應該在f1裏面return的那個Promise的onFulfilledCallbacks裏面,由於只有這樣才能實現讀取完2.txt後纔去打印2.txt的結果。
可是,咱們日常寫promise通常都是這樣寫的: promise.then(f1).then(f2).then(f3)
,一開始全部流程咱們就指定好了,而不是在f1裏面纔去註冊f1的回調,f2裏面纔去註冊f2的回調。
如何既能保持這種鏈式寫法的同時又能使異步操做銜接執行呢?咱們其實讓then方法最後再也不返回自身實例,而是返回一個新的promise便可,咱們能夠叫它bridgePromise,它最大的做用就是銜接後續操做,咱們看下具體實現代碼:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
const self = this;
let bridgePromise;
//防止使用者不傳成功或失敗回調函數,因此成功失敗回調都給了默認回調函數
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
if (self.status === FULFILLED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
})
}
if (self.status === REJECTED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(self.error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
if (self.status === PENDING) {
return bridgePromise = new MyPromise((resolve, reject) => {
self.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
self.onRejectedCallbacks.push((error) => {
try {
let x = onRejected(error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
}
//catch方法實際上是個語法糖,就是隻傳onRejected不傳onFulfilled的then方法
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
//用來解析回調函數的返回值x,x多是普通值也多是個promise對象
function resolvePromise(bridgePromise, x, resolve, reject) {
//若是x是一個promise
if (x instanceof MyPromise) {
//若是這個promise是pending狀態,就在它的then方法裏繼續執行resolvePromise解析它的結果,直到返回值不是一個pending狀態的promise爲止
if (x.status === PENDING) {
x.then(y => {
resolvePromise(bridgePromise, y, resolve, reject);
}, error => {
reject(error);
});
} else {
x.then(resolve, reject);
}
//若是x是一個普通值,就讓bridgePromise的狀態fulfilled,並把這個值傳遞下去
} else {
resolve(x);
}
}
複製代碼
首先,爲防止使用者不傳成功回調函數或不失敗回調函數,咱們給了默認回調函數,而後不管當前promise是什麼狀態,咱們都返回一個bridgePromise用來銜接後續操做。
另外執行回調函數時,由於回調函數既可能會返回一個異步的promise也可能會返回一個同步結果,因此咱們把直接把回調函數的結果託管給bridgePromise,使用resolvePromise方法來解析回調函數的結果,若是回調函數返回一個promise而且狀態仍是pending,就在這個promise的then方法中繼續解析這個promise reslove傳過來的值,若是值仍是pending狀態的promise就繼續解析,直到不是一個異步promise,而是一個正常值就使用bridgePromise的reslove方法將bridgePromise的狀態改成fulfilled,並調用onFulfilledCallbacks回調數組中的方法,將該值傳入,到此異步操做就銜接上了。
這裏很抽象,咱們仍是以文件順序讀取的場景畫一張圖解釋一下流程:
當執行p.then(f1).then(f2).then(f3)
時:
到此,reslove這一條線已經咱們已經走通,讓咱們看看reject這一條線,reject其實處理起來很簡單:
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
複製代碼
到此,咱們已經能夠愉快的使用promise.then(f1).then(f2).then(f3).catch(errorLog)
來順序讀取文件內容了。
其實,到支持串行異步任務這一節,咱們寫的promise在功能上已經基本齊全了,可是還不太規範,好比說一些其餘狀況的判斷等等,這一節咱們就比着Promises/A+的規範打磨一下咱們寫的promise。若是隻是想學習promise的核心實現的,這一節看不懂也不要緊,由於這一節並無增長promise的功能,只是使promise更加規範,更加健壯。
使promise達到Promises/A+規範,經過promises-aplus-tests的完整測試
首先來能夠了解一下Promises/A+規範:
Promises/A+規範原版
Promises/A+規範中文版
相比上一節代碼,本節代碼除了在resolvePromise函數裏增長了幾個其餘狀況的判斷外,其餘函數都沒有修改。完整promise代碼以下:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function MyPromise(fn) {
const self = this;
self.value = null;
self.error = null;
self.status = PENDING;
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
function resolve(value) {
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
if (self.status === PENDING) {
setTimeout(() => {
self.status = FULFILLED;
self.value = value;
self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
}, 0)
}
}
function reject(error) {
if (self.status === PENDING) {
setTimeout(function() {
self.status = REJECTED;
self.error = error;
self.onRejectedCallbacks.forEach((callback) => callback(self.error));
}, 0)
}
}
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
function resolvePromise(bridgepromise, x, resolve, reject) {
//2.3.1規範,避免循環引用
if (bridgepromise === x) {
return reject(new TypeError('Circular reference'));
}
let called = false;
//這個判斷分支其實已經能夠刪除,用下面那個分支代替,由於promise也是一個thenable對象
if (x instanceof MyPromise) {
if (x.status === PENDING) {
x.then(y => {
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
reject(error);
});
} else {
x.then(resolve, reject);
}
// 2.3.3規範,若是 x 爲對象或者函數
} else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
// 是不是thenable對象(具備then方法的對象/函數)
//2.3.3.1 將 then 賦爲 x.then
let then = x.then;
if (typeof then === 'function') {
//2.3.3.3 若是 then 是一個函數,以x爲this調用then函數,且第一個參數是resolvePromise,第二個參數是rejectPromise
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
if (called) return;
called = true;
reject(error);
})
} else {
//2.3.3.4 若是 then不是一個函數,則 以x爲值fulfill promise。
resolve(x);
}
} catch (e) {
//2.3.3.2 若是在取x.then值時拋出了異常,則以這個異常作爲緣由將promise拒絕。
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
const self = this;
let bridgePromise;
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
if (self.status === FULFILLED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
}
if (self.status === REJECTED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(self.error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
if (self.status === PENDING) {
return bridgePromise = new MyPromise((resolve, reject) => {
self.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
self.onRejectedCallbacks.push((error) => {
try {
let x = onRejected(error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
}
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
// 執行測試用例須要用到的代碼
MyPromise.deferred = function() {
let defer = {};
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
}
try {
module.exports = MyPromise
} catch (e) {}
複製代碼
咱們能夠先跑一下測試,須要安裝一下測試插件,而後執行測試,測試時注意在加上上面最後的那幾行代碼才能執行測試用例。
1.npm i -g promises-aplus-tests
2.promises-aplus-tests mypromise.js
複製代碼
運行測試用例能夠看到,咱們上面寫的promise代碼經過了完整的Promises/A+規範測試。
而後開始分析咱們這一節的代碼,咱們主要在resolvePromise里加了額外的兩個判斷,第一個是x和bridgePromise是指向相同值時,報出循環引用的錯誤,使promise符合2.3.1規範,而後咱們增長了一個x 爲對象或者函數的判斷,這一條判斷主要對應2.3.3規範,中文規範如圖:
else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
// 是不是thenable對象(具備then方法的對象/函數)
//2.3.3.1 將 then 賦爲 x.then
let then = x.then;
if (typeof then === 'function') {
//2.3.3.3 若是 then 是一個函數,以x爲this調用then函數,且第一個參數是resolvePromise,第二個參數是rejectPromise
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
if (called) return;
called = true;
reject(error);
})
} else {
//2.3.3.4 若是 then不是一個函數,則以x爲值fulfill promise。
resolve(x);
}
} catch (e) {
//2.3.3.2 若是在取x.then值時拋出了異常,則以這個異常作爲緣由將promise拒絕。
if (called) return;
called = true;
reject(e);
}
}
複製代碼
再寫完這個分支的代碼後,其實咱們已經能夠刪除if (x instanceof MyPromise) {}
這個分支的代碼,由於promise也是一個thenable對象,徹底可使用上述代碼兼容代替。另外,本節代碼不少重複代碼能夠封裝優化一下,可是爲了看得清晰,並無進行抽象封裝,你們若是以爲重複代碼太多的話,能夠自行抽象封裝。
上一節咱們已經實現了一個符合Promises/A+規範的promise,本節咱們把一些es6 promise裏的經常使用方法實現一下。
實現es6 promise的all,race,resolve,reject方法
咱們仍是在以前的基礎上繼續往下寫:
MyPromise.all = function(promises) {
return new MyPromise(function(resolve, reject) {
let result = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(function(data) {
result[i] = data;
if (++count == promises.length) {
resolve(result);
}
}, function(error) {
reject(error);
});
}
});
}
MyPromise.race = function(promises) {
return new MyPromise(function(resolve, reject) {
for (let i = 0; i < promises.length; i++) {
promises[i].then(function(data) {
resolve(data);
}, function(error) {
reject(error);
});
}
});
}
MyPromise.resolve = function(value) {
return new MyPromise(resolve => {
resolve(value);
});
}
MyPromise.reject = function(error) {
return new MyPromise((resolve, reject) => {
reject(error);
});
}
複製代碼
其實前幾節把promise的主線邏輯實現後,這些方法都不難實現,all的原理就是返回一個promise,在這個promise中給全部傳入的promise的then方法中都註冊上回調,回調成功了就把值放到結果數組中,全部回調都成功了就讓返回的這個promise去reslove,把結果數組返回出去,race和all大同小異,只不過它不會等全部promise都成功,而是誰快就把誰返回出去,resolve和reject的邏輯也很簡單,看一下就明白了。
其實到上一節爲止,promise的方法已經都講完了,這一節講一個著名promise庫bluebird裏面的方法promiseify,由於這個方法很經常使用並且之前面試還被問過。promiseify有什麼做用呢?它的做用就是將異步回調函數api轉換爲promise形式,好比下面這個,對fs.readFile 執行promiseify後,就能夠直接用promise的方式去調用讀取文件的方法了,是否是很強大。
let Promise = require('./bluebird');
let fs = require("fs");
var readFile = Promise.promisify(fs.readFile);
readFile("1.txt", "utf8").then(function(data) {
console.log(data);
})
複製代碼
實現bluebird的promiseify方法
MyPromise.promisify = function(fn) {
return function() {
var args = Array.from(arguments);
return new MyPromise(function(resolve, reject) {
fn.apply(null, args.concat(function(err) {
err ? reject(err) : resolve(arguments[1])
}));
})
}
}
複製代碼
雖然方法很強大,可是實現起來並無很難,想在外邊直接調用promise的方法那就返回一個promise唄,內部將原來參數後面拼接一個回調函數參數,在回調函數裏執行這個promise的reslove方法把結果傳出去,promiseify就實現了。
不知不覺寫了這麼多了,你們若是以爲還能夠就給個讚唄,另外每一節的代碼都託管到了github上,你們能夠對照看那一節的promise實現代碼及測試代碼,也順便求個star~
項目地址:本文代碼的github倉庫
另外,實現一個符合Promises/A+規範的promise不止本文一種實現方式,本文只是選取了一種比較通俗易懂的實現方式做爲講解,你們也能夠用本身的方式去實現一個符合Promises/A+規範的promise。