前言
本文旨在簡單講解一下javascript中的Promise對象的概念,特性與簡單的使用方法。并在文末會附上一份符合PromiseA+規范的Promise對象的完整實現。
注:本文中的相關概念均基于PromiseA+規范。
相關參考
正文
1.Promise簡介
在了解javescript中的Promise實現之前有必要先了解一下Promise的概念。
什么是Promise?
關于Promise概念的解釋,網上的各種資料眾說紛紜,這里奉上筆者自己的理解。簡單來說,Promise就是一套處理異步事件的方式和流程。promise在英文中的含義是約定,而針對異步事件特性的處理方式與這個含義非常吻合。
為什么要使用Promise?
一個異步事件不會立刻返回結果,這時我們就需要預先規定一些操作,等待異步事件返回結果后,再去使用某種方式讓預先規定的操作執行。在javascript的習慣中,我們常用回調函數(callback)去實現上述過程。下面是一個簡單的示例:
例1
let asyncFunc = function(callback){
? ? let num = 100;
? ? setTimeout(function(){
? ? ? ? num += 100;
? ? ? ? callback(num);
? ? },2000);
};
function foo(value){
? ? console.log(value);? //value => 200
}
asyncFunc (foo);
上面就是一個簡單的異步操作處理過程,asyncFunc就是一個異步的函數,執行后通過setTimeout方法在2秒返回了一個值,而foo則是一個回調函數,通過傳入異步函數并且在返回結果后被調用的方式獲取異步操作的結果。這里的回調函數就如同一個事先的約定,在異步操作返回結果后立即被實現。
那么,既然js中已經有處理異步事件的方法,為何還要引入Promise這個新的方式呢?實際上,上面這段代碼只是簡單展示下回調函數的基礎使用,而在真正的使用場景中,我們不得不面對各種十分復雜的局面。通常在一個異步操作返回結果后執行的回調中還要進行另一個異步操作,而同一個異步操作返回結果后要執行的回調函數可不止一個。數個異步操作與回調函數彼此嵌套,時刻挑戰者維護和使用者的神經。下面是一個彼此嵌套的例子:
例2
ajax(url1,function(value1){
? ? foo(value1);
? ? bar();
});
function foo(value){
? ? ajax(url2,function(value2){
? ? ? ? do something..
? ? ? ? ajax(url3,function(value3){
? ? ? ? ? ? ...
? ? ? ? })
? ? });
}
function bar(){ do something.. };
上面的例子模擬了一個js中一個常用的異步操作:發送ajax請求數據。在url1請求的回調中使用了foo和bar兩個函數,而foo中又發送了url2,url3的請求。。。這樣數層嵌套下來,最終導致代碼非常的不直觀,維護起來難度也直線上升,形成常說的“回調地獄”。
了解了傳統上js處理異步操作的復雜和困難后,我們不禁思索,是否有方法能夠更加簡潔,直觀的去解決異步操作的種種問題?答案就是我們這篇文章的主角:Promise。
2. Promise的特性及使用
在PromiseA+規范中做出了這樣定義:promise是一個包含了兼容Promise規范then方法的對象或函數,與Promise最主要的交互方法是通過將函數傳入它的then方法從而獲取得Promise最終的值或Promise最終最拒絕(reject)的原因。
? 這段定義有兩個重點:1.Promise是一個對象或函數? 2.它有一個then方法,能夠獲取prmose的最終結果。下面我們就來實際看一下Promise到底是如何處理異步事件的,我們將上面的例1使用Promise進行一下改寫:
例3
let p = new Promise(function(resolve,reject){
? ? let value = 100;
? ? setTimeout(function(){
? ? ? ? value += 100;
? ? ? ? resolve(value);
? ? },2000);
});
p.then(function(value){
? ? console.log(value);? ? ? //value => 200
},function(err){
? ? do something...
});
初看之下其實并沒有太大區別,但實際上Promise的威力在更復雜的場景下才能更好的發揮。我們先針對這個簡單的例子來講解下Promise的使用
首先通過 new 關鍵字實例化一個Promise對象,在這個對象中傳入一個要執行異步操作的函數。這個函數包含兩個形參:resolve和reject。這兩個形參是Promise中定義的2個函數,分別在異步事件成功和失敗時調用。例3中我們在2秒后調用了resolve函數,代表著異步事件成功,返回一個值。而在我們實例化Promise對象的同時,我們又調用了這個實例的then方法。then方法可以說是Promise方法中的核心,它即代表著Promise約定的這層含義,在then方法中接收2個函數作為參數,分別在異步事件成功時或失敗時執行,并且兩個函數的參數正是異步事件成功時返回的值或失敗時原因。
其實,使用Promise對象來處理異步事件比起使用傳統的回調函數的一個優點在于:Promise規范了處理異步事件的流程。我們不必再深入異步事件的內部,去分析種種狀態變化后對應的回調究竟如何調用,也不必過多考慮異步事件內部發生錯誤時該如何捕獲,我們只需要在合適的時候通知Promise返回成功或失敗狀態,剩下的事統統交給Promise去解決。
以上我們大致了解了Promise的處理流程,在詳細講解Promise對象中的方法之前有必要先了解一下Promise的狀態概念。
一個Promise對象在實例化后可能擁有以下3種狀態的其中之一:
Fulfilled - 當傳入的異步事件成功返回值時的狀態
Rejected - 當傳入的異步事件失敗或產生異常時的狀態
Pending -? 當傳入的異步事件還沒有結果返回時的狀態
注意,任何時候Promise對象都只能處于以上其中狀態的一種,當Promise對象處于Pending狀態時,它可以轉化成Fulfilled 或Rejected 狀態,而當Promise對象處于Fulfilled 或Rejected狀態時,它不能再轉化成其他狀態。
可以用一張圖來直白的表示上面這段話
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?(圖片取自Promise迷你書)
在了解了Promise的三種狀態后 ,接下來可以詳細了解下Promise對象的幾個方法
resolve()
resolve方法是在一個Promise對象實例化時傳入的任務函數的第一個參數,它的作用是讓Promise進入“Fulfilled ”狀態,resolve方法只接受一個參數,即異步事件的返回值value。
reject()
reject方法與resolve方法正好相反,它是在一個Promise對象實例化時傳入的任務函數的第二個參數,它的作用是讓Promise進入“Rejected”狀態,reject方法同樣只接受一個參數,即異步事件失敗或異常的原因reason。
Promise.prototype.then()
then方法是Promise對象方法的重中之重,它是Promise實例的方法,用來注冊Promise對象成功時執行的回調函數(onFulfilled)和失敗時執行的回調函數(onRejected)。一個then方法的返回值仍然是一個Promsie對象。因此,then方法支持鏈式調用,也就是一個一個then方法的返回值可以繼續調用then。而相鏈接的then方法中,在上一個then方法的onFulfilled或onRejected回調函數中通過 return value(reason)的方式,把這個結果作為下一個then中的回調函數的參數被接收。onFulfilled和onRejected函數的返回值可以是任何javascript值,甚至一個Promise對象的成功或失敗時的回調函數可以返回一個新的Promise對象。這樣的特性使得例2中那種復雜的異步事件嵌套的場景處理得以簡化。下面是使用Promise來重寫的例2:
例4
let p1 = new Promise(function(resolve,reject){
? ? ajax(url1,function(value1){
? ? ? ? resolve(value1);
? ? });
});
p1.then(function(value1){
? ? return new Promise(function(resolve,reject){
? ? ? ? ajax(url2,function(value2){
? ? ? ? ? ? do something..
? ? ? ? ? ? resolve(value2);
? ? ? ? });
? ? })
}).then(function(value2){
? ? return new Promise(function(resolve,reject){
? ? ? ? ajax(url3,function(value3){
? ? ? ? ? ? ...
? ? ? ? });
? ? })
});
p1.then(bar);
function bar(){do something...};
可以看出,使用Promise改寫后的代碼結構上更加清晰,它把層層嵌套的函數轉化成鏈式的調用then方法的形式,這樣可以非常清晰的看出事件間的關系和執行順序,大大降低了日后代碼使用和維護的難度。
關于then方法還有幾點補充:
1. then方法中的onFulfilled和onRejected方法都是可以省略的。
2. 當一個Promise失敗返回了reason,而then方法中沒有定義onRejected函數時,這個reason會被鏈式調用的下一個then方法的onRejected方法接收。
3. 一個Promise實例可以調用多次then方法,這些then注冊的onFulfilled和onRejected函數會按照注冊的順序執行。
Promise.prototype.catch()
catch方法是一個then方法的語法糖,它只接受一個失敗處理函數onRejected,實際上等同于以下代碼:
new Promsie.then(null,function(){
? ? do something...
})
Promise.all()
all方法是Promsie對象的靜態方法,使用方式是 Promise.all()。all方法接收的參數為一個包含數個Promise對象實例的數組,并返回一個新的Promise實例。當數組中所有的Promse實例都返回結果后,將所有數組中的Promise實例的成功返回值傳入一個數組,并將這個數組注入到all方法返回的新實例的then方法中。下面是一個all方法的使用實例:
例5
let promiseArr = [
? ? new Promise(function(resolve,reject){
? ? ? ? setTimeout(function(){
? ? ? ? ? ? resolve(100)
? ? ? ? },1000)
? ? }),
? ? new Promise(function(resolve,reject){
? ? ? ? setTimeout(function(){
? ? ? ? ? ? resolve(200)
? ? ? ? },500)
? ? })
]
Promise.all(promiseArr).then(function(valArr){
? ? console.log(valArr)? ? // valArr? => [100,200]
},function(err){
? ? do something...
})
all方法值得注意的有兩點:
1.數組中所有promise實例都成功后的返回值,在valArr中的順序是按照promiseArr 中promise實例的順序來排列的。
2.當任何一個promise失敗后,all方法直接將返回的Promise對象的狀態變為Rejected,并調用then方法的onRejected函數,把失敗的原因傳遞出來。
Promise.resolve()
Promsie對象本身存在一個resolve方法,它的作用是立刻返回一個狀態為Fulfilled的Promise對象實例。如果你在這個resolve方法中傳入的是一個Promise實例的話,那么resolve方法會保持這個Promise實例的狀態,并根據它最后返回的狀態來調用resolve方法返回的Promise實例then方法的onResolve或onRejected函數。
其實這個方法最常用的場景是講一個普通的值轉換成一個Promise實例。一般來說不是很常用。
Promise.reject()
與Promise.resolve()相反,它的作用是立刻返回一個狀態為Rejected的Promise對象實例。實際上這個方法是一個語法糖,它等同于以下代碼:
new Promise(function(resolve,reject){
? ? reject(reason);
})
以上就是一個ES6中的Promise對象中所包含的常用方法。
3. 一個符合PromiseA+規范的Promise對象的完整實現
? 想必看到這里的一些讀者會不禁思考,Promise對象究竟是如何實現的呢?我個人參考了一些資料實現了一個符合PromiseA+規范的Promise對象,把源代碼貼在下面,有興趣的朋友可以參考一下,實際上代碼本身并不是很多,各位看完以后可以嘗試用自己的方式再實現一遍。同時附上一個測試工具,里面包含了幾百個測試用例,用來測試我們自己寫的Promise是否完美的符合PromiseA+規范。
使用的方法很簡單
npm i -g promises-aplus-tests
promises-aplus-tests Promise.js
安裝后運行你的js文件就可以測試你的代碼是否符合規范了。
下面就是我實現的Promise對象的代碼
function MyPromise(task) {
? ? const _this = this;
? ? _this.status = 'pending';? //設定初始狀態
? ? _this.value = undefined;
? ? _this.onFulfilledsList = [];? //onFulfilled函數序列
? ? _this.onRejectedsList = [];? //onRejected函數序列
? ? function resolve(value) {
? ? ? ? if (value instanceof MyPromise) {
? ? ? ? ? ? return value.then(resolve, reject);
? ? ? ? }
? ? ? ? //異步執行resolve或reject方法,保證代碼的統一性和注冊的回調函數按照正確的順序執行
? ? ? ? ? ? if (_this.status === 'pending') {
? ? ? ? ? ? ? ? _this.status = 'fulfilled';
? ? ? ? ? ? ? ? _this.value = value;
? ? ? ? ? ? ? ? _this.onFulfilledsList.forEach(cb => cb(value))
? ? ? ? ? ? }
? ? }
? ? function reject(reason) {
? ? ? ? ? ? if (_this.status === 'pending') {
? ? ? ? ? ? ? ? _this.status = 'rejected';
? ? ? ? ? ? ? ? _this.reason = reason;
? ? ? ? ? ? ? ? _this.onRejectedsList.forEach(cb => cb(reason))
? ? ? ? ? ? }
? ? }
? ? try {
? ? ? ? task(resolve, reject);
? ? } catch (err) {
? ? ? ? throw new Error(err);
? ? }
}
function resolvePromise(promise2, x, resolve, reject) {
? ? if (x === promise2) {
? ? ? ? return reject(new TypeError('循環引用'));
? ? }
? ? //如果返回的是一個thenable對象,即一個擁有then方法的對象,那么使用它的then方法去獲得它的最終返回值。目的是為了兼容其他Promise庫
? ? if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
? ? ? ? let then, called;
? ? ? ? try {
? ? ? ? ? ? then = x.then;
? ? ? ? ? ? if (typeof then === 'function') {
? ? ? ? ? ? ? ? then.call(x, function (newx) {
? ? ? ? ? ? ? ? ? ? if (called) return;? //防止重復調用
? ? ? ? ? ? ? ? ? ? called = true;
? ? ? ? ? ? ? ? ? ? resolvePromise(promise2, newx, resolve, reject);
? ? ? ? ? ? ? ? }, function (err) {
? ? ? ? ? ? ? ? ? ? if (called) return;
? ? ? ? ? ? ? ? ? ? called = true;
? ? ? ? ? ? ? ? ? ? return reject(err);
? ? ? ? ? ? ? ? });
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? resolve(x);
? ? ? ? ? ? }
? ? ? ? } catch (err) {
? ? ? ? ? ? if (called) return;
? ? ? ? ? ? called = true;
? ? ? ? ? ? reject(err);
? ? ? ? }
? ? } else {
? ? ? ? resolve(x);
? ? }
}
MyPromise.prototype.then = function (onFulfilled, onRejected) {
? ? const _this = this;
? ? let promise2;
? ? onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (data) {
? ? ? ? return data;
? ? };
? ? onRejected = typeof onRejected === 'function' ? onRejected : function (data) {
? ? ? ? throw data;
? ? };
? ? //為了支持同步代碼,當then方法注冊的時候如果Promise的狀態已經改變,那么立即執行對應的函數
? ? if (_this.status === 'fulfilled') {
? ? ? ? promise2 = new MyPromise(function (resolve, reject) {
? ? ? ? ? setTimeout(function () {
? ? ? ? ? ? let x;
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? x = onFulfilled(_this.value);
? ? ? ? ? ? ? ? resolvePromise(promise2, x, resolve, reject);
? ? ? ? ? ? } catch (err) {
? ? ? ? ? ? ? ? reject(err);
? ? ? ? ? ? }
? ? ? ? ? })
? ? ? ? })
? ? }
? ? if (_this.status === 'rejected') {
? ? ? ? promise2 = new MyPromise(function (resolve, reject) {
? ? ? ? ?setTimeout(function () {
? ? ? ? ? ? let x;
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? x = onRejected(_this.reason);
? ? ? ? ? ? ? ? resolvePromise(promise2, x, resolve, reject);
? ? ? ? ? ? } catch (err) {
? ? ? ? ? ? ? ? reject(err);
? ? ? ? ? ? }
? ? ? ? ?)}
? ? ? ? })
? ? }
? ? if (_this.status === 'pending') {
? ? ? ? promise2 = new MyPromise(function (resolve, reject) {
? ? ? ? ? ? _this.onFulfilledsList.push(function (value) {
? ? ? ? ????????setTimeout(function () {
? ? ? ? ? ? ? ? let x;
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? x = onFulfilled(value);
? ? ? ? ? ? ? ? ? ? resolvePromise(promise2, x, resolve, reject);
? ? ? ? ? ? ? ? } catch (err) {
? ? ? ? ? ? ? ? ? ? reject(err);
? ? ? ? ? ? ? ? }
????????????????})
? ? ? ? ? ? });
? ? ? ? ? ? _this.onRejectedsList.push(function (reason) {
? ? ? ? ? ? ? ?setTimeout(function () {
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? let x = onRejected(reason);
? ? ? ? ? ? ? ? ? ? resolvePromise(promise2, x, resolve, reject);
? ? ? ? ? ? ? ? } catch (err) {
? ? ? ? ? ? ? ? ? ? reject(err);
? ? ? ? ? ? ? ? }
????????????})
????????});
? ? ? ? })
? ? }
? ? return promise2;? //返回一個新的Promise實例,以便支持鏈式調用
};
MyPromise.prototype.catch = function (onRejected) {
? ? this.then(null, onRejected);
};
MyPromise.all = function (someValue) {
? ? let resolveValArr = [];
? ? let count = promiseLen = 0;
? ? let promise2;
? ? promise2 = new MyPromise(function (resolve, reject) {
? ? ? ? let iNow = 0;
? ? ? ? try {
? ? ? ? ? ? for (let item of someValue) {
? ? ? ? ? ? ? ? if (item !== null && typeof item === "object") {
? ? ? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? ? ? let then = item.then;
? ? ? ? ? ? ? ? ? ? ? ? let index = iNow;
? ? ? ? ? ? ? ? ? ? ? ? if (typeof then === 'function') {
? ? ? ? ? ? ? ? ? ? ? ? ? ? promiseLen++;
? ? ? ? ? ? ? ? ? ? ? ? ? ? then.call(item, function (value) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? resolveValArr[index] = value;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? if (++count === promiseLen) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? resolve(resolveValArr)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? ? ? }, function (err) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? reject(err);
? ? ? ? ? ? ? ? ? ? ? ? ? ? });
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? } catch (err) {
? ? ? ? ? ? ? ? ? ? ? ? resolveValArr[iNow] = item;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? resolveValArr[iNow] = item;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? iNow++;
? ? ? ? ? ? }
? ? ? ? ? ? if (iNow === 0) {
? ? ? ? ? ? ? ? return resolve(someValue);
? ? ? ? ? ? }
? ? ? ? ? ? if (promiseLen === 0) {
? ? ? ? ? ? ? ? return resolve(resolveValArr);
? ? ? ? ? ? }
? ? ? ? } catch (err) {
? ? ? ? ? ? reject(new TypeError('無法遍歷的類型!'));
? ? ? ? }
? ? });
? ? return promise2;
};
MyPromise.race = function (someValue) {
? ? let promise2;
? ? promise2 = new MyPromise(function (resolve, reject) {
? ? ? ? let iNow = 0;
? ? ? ? try {
? ? ? ? ? ? for (let item of someValue) {
? ? ? ? ? ? ? ? if (item !== null && typeof item === "object") {
? ? ? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? ? ? let then = item.then;
? ? ? ? ? ? ? ? ? ? ? ? then.call(item, function (value) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? resolve(value);
? ? ? ? ? ? ? ? ? ? ? ? }, function (err) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? reject(err);
? ? ? ? ? ? ? ? ? ? ? ? });
? ? ? ? ? ? ? ? ? ? } catch (err) {
? ? ? ? ? ? ? ? ? ? ? ? resolve(item);
? ? ? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? resolve(item);
? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? iNow++;
? ? ? ? ? ? }
? ? ? ? ? ? if (iNow === 0) {
? ? ? ? ? ? ? ? return resolve(someValue);
? ? ? ? ? ? }
? ? ? ? } catch (err) {
? ? ? ? ? ? reject(new TypeError('無法遍歷的類型!'));
? ? ? ? }
? ? });
? ? return promise2;
};
MyPromise.resolve = function (value) {
? ? let promise2;
? ? if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
? ? ? ? promise2 = new MyPromise(function (resolve, reject) {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? let then = value.then;
? ? ? ? ? ? ? ? if (typeof value.then === 'function') {
? ? ? ? ? ? ? ? ? ? then.call(value, function (data) {
? ? ? ? ? ? ? ? ? ? ? ? resolve(data);
? ? ? ? ? ? ? ? ? ? }, reject);
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? resolve(value);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } catch (err) {
? ? ? ? ? ? ? ? reject(err);
? ? ? ? ? ? }
? ? ? ? })
? ? } else {
? ? ? ? promise2 = new MyPromise(function (resolve) {
? ? ? ? ? ? resolve(value);
? ? ? ? })
? ? }
? ? return promise2;
};
MyPromise.reject = function (reason) {
? ? return new MyPromise(function (resolve, reject) {
? ? ? ? reject(reason);
? ? })
};
module.exports = MyPromise;
//這是為了讓代碼能夠測試而開放的接口,詳見promises-aplus-tests中的相關描述
MyPromise.deferred = MyPromise.defer = function () {
? ? let deferred = {};
? ? deferred.promise = new MyPromise(function (resolve, reject) {
? ? ? ? deferred.resolve = resolve;
? ? ? ? deferred.reject = reject;
? ? });
? ? return deferred
};
尾聲
本文參考了很多資料,如果你看到其他文章有類似的觀點非常正常,不過筆者盡量使用了自己的理解去闡述Promise的相關知識。如果你發現本文中有哪些疏漏,歡迎發私信給我進行斧正。同時也可以在下面留言給我,我會一一查看,盡量回復。