在上篇文章裡《JavaScript基礎——回撥(callback)是什麼》我們一起學習了回撥,明白了回撥就是一個在另外一個函式執行完後要執行的函式,如果我們希望非同步函式能夠像同步函式那樣順序執行,只能巢狀使用回撥函式,過多的回撥巢狀會使得程式碼變得難以理解與維護,為了避免“回撥地獄”讓人發狂的行為,ES6原生引入了promise的模式,通過這種方式,讓我們程式碼看起來像同步程式碼,大大簡化了非同步程式設計,簡直是ES6新特性中最讓我們興奮的特性之一。
什麼是promise?
首先我們看看promise這個單詞的中文釋義,作為名稱解釋為承諾、諾言、誓言、約言,從中文釋義可以看出,是一個未發生,將來一定會發生的某種東東…… 接下來我們來看看ECMA委員會怎麼定義Promise的:
A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.
Promise是一個物件,用作佔位符,用於延遲(可能是非同步)計算的最終結果。
簡單的來說,promise就是裝載未來值的容器。其實生活中有很多Promise的場景,設想以下的場景:我們去快餐店點餐,你點了一份牛肉麵,你掃碼付款後,會拿到一份帶訂單號的收據。訂單號就是快餐店給我們的一份牛肉麵的承諾(promise),保證了我們得到一份牛肉麵。
所以我們一定要包管好我們的訂單收據,因為我們知道了這個收據代表了我們未來會有一份牛肉麵,儘管快餐店不能馬上給我們一份牛肉麵,但是我們大腦潛意識的把訂單收據當做牛肉麵的“佔位符”了
終於,我們聽到服務員在喊“100號的牛肉麵好了,請到視窗取餐”,然後我們拿著訂單收據來到視窗遞給服務員,我們換來了牛肉麵。
說了這麼多,簡單點就是,一旦我需要的值準備好了,我們就用我的承諾值換取這個值本身。
但是,還有一種結果,服務員叫到我們的訂單號,當我們去拿的時候,服務員會一臉歉意的告訴我們“十分抱歉,您的牛肉麵賣完了”。作為顧客的我們對這個情況,除了憤怒之外只能換個地吃飯了或者點其他的。從中我們看出,未來值還有一個重要的特性:它可能成功也可能失敗。
生活的例子很簡單,我們是不是特別著急如何用程式碼編寫Promise呢,在編寫程式碼之前,我們還是先聊聊——Promise State(承諾狀態,注:暫且這麼翻譯,小編也不知道如何翻譯更好)
Promise State(承諾狀態)
Promise只會處在以下狀態之一:
Pending(待處理): promise初始化的狀態,正在執行,既未完成也沒有失敗的狀態,此狀態可以遷移至fulfilled和rejected狀態。
Fulfilled(已完成):如果回撥函式實現Promise的resolve回撥(稍後介紹),那我們的promise實現兌現。
Rejected(已拒絕):如果Promise呼叫過程中遭到拒絕或者發生異常,那麼我們的promise被拒絕,處於Rejected(狀態)。
Settled(不變的):Promise如果不處在Pending狀態,狀態就會改變,要不是Fulfilled要不是Rejected這兩種狀態。
Promise的狀態轉換,可以用下面一張圖進行表示(圖片來源:developer.mozilla.org/en-US/docs/…)
Promise vs callback
比如我們有個需求,需要通過AJAX實現三個請求,第二個和第三個請求都依賴上一個介面的請求,如果使用CallBack的方式,我們的程式碼可能是這樣的:
ajaxCall('http://example.com/page1', response1 => {
ajaxCall('http://example.com/page2'+response1, response2 => {
ajaxCall('http://example.com/page3'+response2, response3 => {
console.log(response3)
}
})
})複製程式碼
你很快就會發現,這種多重巢狀的程式碼不但難以理解,而且難以維護,這就是著名的“回撥地獄”現象。
如果使用Promise則會讓我們的大腦更容易接受和理解,程式碼顯得簡單扁平化,程式碼如下(虛擬碼),如何實現ajaxCallPromise稍後介紹:
ajaxCallPromise('http://example.com/page1')
.then( response1 => ajaxCallPromise('http://example.com/page2'+response1) )
.then( response2 => ajaxCallPromise('http://example.com/page3'+response2) )
.then( response3 => console.log(response3) )複製程式碼
你是不是覺得程式碼的複雜性突然降低,程式碼看起來更簡單易讀呢,各位是不是特別著急ajaxCallPromise是如何實現的吧,接著看下一小節,會有介紹。
Promise實現——(resolve, reject) 方法
要實現回撥函式轉換成Promise,我們需要使用Promise建構函式,在上一小節,我們的示例ajaxCallPromise函式返回Promise,我們可以自定義resolve實現和reject實現,實現函式如下:
const ajaxCallPromise = url => {
return new Promise((resolve, reject) => {
// DO YOUR ASYNC STUFF HERE
$.ajaxAsyncWithNativeAPI(url, function(data) {
if(data.resCode === 200) {
resolve(data.message)
} else {
reject(data.error)
}
})
})
}複製程式碼
如何理解這段程式碼呢?
- 首先定義ajaxCallPromise返回型別為Promise,這意味我們會實現一個Promise的承諾。
- Promise接受兩個函式引數,resolve(成功實現承諾)和reject(異常導致失敗)
- resolve和reject這兩個特有的方法,會獲取對應成功或失敗的值
- 如果介面請求一切正常,我們將會接收到resolve函式返回的值
- 如果介面請求失敗,我們將會接收到reject返回的的值
在舉個簡單的例子,如果foo()和bar()函式都實現promise,我們怎麼書寫程式碼呢?
方式一:
foo().then( res => {
bar().then( res2 => {
console.log('Both done')
})
})複製程式碼
方式二(建議這種,簡單易讀)
foo()
.then( res => bar() ) // bar() returns a Promise
.then( res => {
console.log('Both done')
})複製程式碼
.then(onFulfilled, onRejected) 方法
Promise的then()方法允許我們在任務完成後或拒絕失敗後執行任務,該任務可以是基於另外一個事件或基於回撥的非同步操作。
Promise的then()方法接收兩個引數,即onFulfilled 和 onRejected 的回撥,如果Promise物件完成,則執行onFulfilled回撥,如果執行異常或失敗執行onRejected回撥。
簡單的來說,onFulfilled回撥接收一個引數,及所謂的未來的值,同樣 onRejected 也接收一個引數,顯示拒絕的原因。讓我們動下上小節ajaxCallPromise的then()方法:
ajaxCallPromise('http://example.com/page1').then(
successData => { console.log('Request was successful') },
failData => { console.log('Request failed' + failData) }
)複製程式碼
如果請求過程失敗,第二個函式將會執行輸出而不是第一個函式輸出。
我們一起再來看個簡單的例子,我們在setTimeout()實現Promise回撥,程式碼如下:
const PsetTimeout = duration => {
return new Promise((resolve, reject) => {
setTimeout( () => {
resolve()
}, duration);
})
}
PsetTimeout(1000)
.then(() => {
console.log('Executes after a second')
})複製程式碼
這裡我們在這裡實現了一個成功狀態後沒有返回值的Promise,如果這樣做,成功返回後未來值將會是 undefined.
catch(onRejected)方法
除了then()方法可以處理錯誤和異常,使用Promise的catch()方法也能實現同樣的功能,這個方法其實並沒有什麼特別,只是更容易理解而已。
catch()方法只接收一個引數,及onRejected回撥。catch()方法的onRejected回撥的呼叫方式與then()方法的onRejected回撥相同。
還記得我們上小節ajaxCallPromise的then()方法的實現嗎:
ajaxCallPromise('http://example.com/page1').then(
successData => { console.log('Request was successful') },
failData => { console.log('Request failed' + failData) }
)複製程式碼
我們還可以使用catch()方法進行捕獲異常或拒絕,效果是一致的。
ajaxCallPromise('http://example.com/page1’)
.then(successData => console.log('Request was successful’))
.catch(failData => console.log('Request failed' + failData));複製程式碼
Promise.resolve(value)方法
Promise的resolve()方法接收成功返回值並返回一個Promise物件,用於未來值的傳遞,將值傳遞給.then(onFulfilled, onRejected) 的onFulfilled回撥中。resolve()方法可以用於將未來值轉化成Promise物件,下面的一段程式碼演示瞭如何使用Promise.resolve()方法:
const p1 = Promise.resolve(4);
p1.then(function(value){
console.log(value);
}); //passed a promise object
Promise.resolve(p1).then(function(value){
console.log(value);
});
Promise.resolve({name: "Eden"})
.then(function(value){
console.log(value.name);
});複製程式碼
控制檯將會輸出以下內容:
4
4
Eden複製程式碼
Promise.reject(value)方法
Promise.reject(value)方法可上小節Promise.resolve(value)類似,唯一不同的是將值傳遞給.then(onFulfilled, onRejected) 的onRejected回撥中,同時Promise.reject(value)主要用來進行除錯,而不是將值轉換成Promise物件。
讓我們看看下面一段程式碼,看看如何使用Promise.reject(value)方法:
const p1 = Promise.reject(4);
p1.then(null, function(value){
console.log(value);
});
Promise.reject({name: "Eden"})
.then(null, function(value){
console.log(value.name);
});複製程式碼
控制檯將輸出:
4
Eden複製程式碼
Promise.all(iterable) 方法
該方法可以傳入一個可以迭代Promise物件,比如陣列並返回一個Promise物件,當所有的Promise迭代物件成功返回後,整個Promise才能返回成功狀態的值。
好了,我們一起看看怎麼實現Promise.all(iterable) 方法:
const p1 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 1000);
});
const p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 2000);
});
const arr = [p1, p2];
Promise.all(arr).then(function(){
console.log("Done"); //"Done" is logged after 2 seconds
});複製程式碼
特別需要注意的一點,只要迭代陣列中,只要任意一個進入失敗狀態,那麼該方法返回的物件也會進入失敗狀態,並將那個進入失敗狀態的錯誤資訊作為自己的錯誤資訊,示例程式碼如下:
const p1 = new Promise(function(resolve, reject){
setTimeout(function(){
reject("Error");
}, 1000);
});
const p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 2000);
});
const arr = [p1, p2];
Promise.all(arr).then(null, function(reason){
console.log(reason); //"Error" is logged after 1 second
});複製程式碼
Promise.race(iterable) 方法
與Promise.all(iterable) 不同的是,Promise.race(iterable) 雖然也接收包含若干個Promise物件的可迭代物件,不同的是這個方法會監聽所有的Promise物件,並等待其中的第一個進入完成或失敗狀態的Promise物件,一旦有Promise物件滿足,整個Promise物件將返回這個Promise物件的成功狀態或失敗狀態,下面的示例展示了返回第一個成功狀態的值:
var p1 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("Fulfillment Value 1");
}, 1000);
});
var p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("fulfillment Value 2");
}, 2000);
});
var arr = [p1, p2];
Promise.race(arr).then(function(value){
console.log(value); //Output "Fulfillment value 1"
}, function(reason){
console.log(reason);複製程式碼
用Promise改寫上篇文章的回撥方法
讀過《JavaScript基礎——回撥(callback)是什麼》文章同學,文章的最後我們用回撥函式實現了一個真實的業務場景——用NodeJs實現從論壇帖子列表中顯示其中的一個帖子的資訊及留言列表資訊,如果使用本篇文章學習到的內容,我們如何實現呢, 程式碼如下:
index.js
const fs = require('fs');
const path = require('path');
const postsUrl = path.join(__dirname, 'db/posts.json');
const commentsUrl = path.join(__dirname, 'db/comments.json');
//return the data from our file
function loadCollection(url) {
return new Promise(function(resolve, reject) {
fs.readFile(url, 'utf8', function(error, data) {
if (error) {
reject('error');
} else {
resolve(JSON.parse(data));
}
});
});
}
//return an object by id
function getRecord(collection, id) {
return new Promise(function(resolve, reject) {
const data = collection.find(function(element){
return element.id == id;
});
resolve(data);
});
}
//return an array of comments for a post
function getCommentsByPost(comments, postId) {
return comments.filter(function(comment){
return comment.postId == postId;
});
}
//initialization code
loadCollection(postsUrl)
.then(function(posts){
return getRecord(posts, "001");
})
.then(function(post){
console.log(post);
return loadCollection(commentsUrl);
})
.then(function(comments){
const postComments = getCommentsByPost(comments, "001");
console.log(postComments);
})
.catch(function(error){
console.log(error);
});複製程式碼
結束語:
本篇的內容就介紹到這裡,各位是否看的很過癮,雖然Promise已經比回撥函式好許多,但是還是不夠簡潔,不夠符合我們人類大腦思考邏輯,如果我們能以書寫同步的方法書寫非同步程式碼,那該多好啊,ES8引入了async/await讓我們能用同步的方式書寫非同步程式碼,想想就很激動,小編將會在下篇文章進行介紹,敬請期待
更多精彩內容,請微信關注”前端達人”公眾號!