JavaScript基礎——Promise使用指南

前端達人發表於2019-01-03

在上篇文章裡《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/…

JavaScript基礎——Promise使用指南

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) 
  } 
 }) 
 }) 
}複製程式碼

如何理解這段程式碼呢?

  1. 首先定義ajaxCallPromise返回型別為Promise,這意味我們會實現一個Promise的承諾。
  2. Promise接受兩個函式引數,resolve(成功實現承諾)和reject(異常導致失敗)
  3. resolve和reject這兩個特有的方法,會獲取對應成功或失敗的值
  4. 如果介面請求一切正常,我們將會接收到resolve函式返回的值
  5. 如果介面請求失敗,我們將會接收到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讓我們能用同步的方式書寫非同步程式碼,想想就很激動,小編將會在下篇文章進行介紹,敬請期待

更多精彩內容,請微信關注”前端達人”公眾號!

JavaScript基礎——Promise使用指南


相關文章