Promise入門詳解和基本用法
非同步呼叫
非同步
JavaScript的執行環境是單執行緒。
所謂單執行緒,是指JS引擎中負責解釋和執行JavaScript程式碼的執行緒只有一個,也就是一次只能完成一項任務,這個任務執行完後才能執行下一個,它會「阻塞」其他任務。這個任務可稱為主執行緒。
非同步模式可以一起執行多個任務。
常見的非同步模式有以下幾種:
-
定時器
-
介面呼叫
-
事件函式
今天這篇文章,我們重點講一下介面呼叫。介面呼叫裡,重點講一下Promise。
介面呼叫的方式
js 中常見的介面呼叫方式,有以下幾種:
- 原生ajax
- 基於jQuery的ajax
- Fetch
- Promise
- axios
多次非同步呼叫的依賴分析
-
多次非同步呼叫的結果,順序可能不同步。
-
非同步呼叫的結果如果存在依賴,則需要巢狀。
在ES5中,當進行多層巢狀回撥時,會導致程式碼層次過多,很難進行維護和二次開發;而且會導致回撥地獄的問題。ES6中的Promise 就可以解決這兩個問題。
Promise 概述
Promise的介紹和優點
ES6中的Promise 是非同步程式設計的一種方案。從語法上講,Promise 是一個物件,它可以獲取非同步操作的訊息。
Promise物件, 可以將非同步操作以同步的流程表達出來。使用 Promise 主要有以下好處:
-
可以很好地解決回撥地獄的問題(避免了層層巢狀的回撥函式)。
-
語法非常簡潔。Promise 物件提供了簡潔的API,使得控制非同步操作更加容易。
回撥地獄的舉例
假設買菜、做飯、洗碗都是非同步的。
但真實的場景中,實際的操作流程是:買菜成功之後,才能開始做飯。做飯成功後,才能開始洗碗。這裡面就涉及到了多層巢狀呼叫,也就是回撥地獄。
Promise 的基本用法
(1)使用new例項化一個Promise物件,Promise的建構函式中傳遞一個引數。這個引數是一個函式,該函式用於處理非同步任務。
(2)並且傳入兩個引數:resolve和reject,分別表示非同步執行成功後的回撥函式和非同步執行失敗後的回撥函式;
(3)通過 promise.then() 處理返回結果。這裡的 p 指的是 Promise例項。
程式碼舉例如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> // 第一步:model層的介面封裝 const promise = new Promise((resolve, reject) => { // 這裡做非同步任務(比如ajax 請求介面。這裡暫時用定時器代替) setTimeout(function() { var data = { retCode: 0, msg: 'qianguyihao' }; // 介面返回的資料 if (data.retCode == 0) { // 介面請求成功時呼叫 resolve(data); } else { // 介面請求失敗時呼叫 reject({ retCode: -1, msg: 'network error' }); } }, 100); }); // 第二步:業務層的介面呼叫。這裡的 data 就是 從 resolve 和 reject 傳過來的,也就是從介面拿到的資料 promise.then(data => { // 從 resolve 獲取正常結果 console.log(data); }).catch(data => { // 從 reject 獲取異常結果 console.log(data); }); </script> </body> </html>
上方程式碼中,當從介面返回的資料data.retCode
的值不同時,可能會走 resolve,也可能會走 reject,這個由你自己的業務決定。
promise物件的3個狀態(瞭解即可)
-
初始化狀態(等待狀態):pending
-
成功狀態:fullfilled
-
失敗狀態:rejected
(1)當new Promise()執行之後,promise物件的狀態會被初始化為pending
,這個狀態是初始化狀態。new Promise()
這行程式碼,括號裡的內容是同步執行的。括號裡定義一個function,function有兩個引數:resolve和reject。如下:
-
如果請求成功了,則執行resolve(),此時,promise的狀態會被自動修改為fullfilled。
-
如果請求失敗了,則執行reject(),此時,promise的狀態會被自動修改為rejected
(2)promise.then()方法,括號裡面有兩個引數,分別代表兩個函式 function1 和 function2:
-
如果promise的狀態為fullfilled(意思是:如果請求成功),則執行function1裡的內容
-
如果promise的狀態為rejected(意思是,如果請求失敗),則執行function2裡的內容
另外,resolve()和reject()這兩個方法,是可以給promise.then()傳遞引數的。
完整程式碼舉例如下:
let promise = new Promise((resolve, reject) => {
//進來之後,狀態為pending
console.log('111'); //這行程式碼是同步的
//開始執行非同步操作(這裡開始,寫非同步的程式碼,比如ajax請求 or 開啟定時器)
if (非同步的ajax請求成功) {
console.log('333');
resolve('haha');//如果請求成功了,請寫resolve(),此時,promise的狀態會被自動修改為fullfilled
} else {
reject('555');//如果請求失敗了,請寫reject(),此時,promise的狀態會被自動修改為rejected
}
})
console.log('222');
//呼叫promise的then()
promise.then((successMsg) => {
//如果promise的狀態為fullfilled,則執行這裡的程式碼
console.log(successMsg, '成功了');
}
, (errorMsg) => {
//如果promise的狀態為rejected,則執行這裡的程式碼
console.log(errorMsg, '失敗了');
}
)
基於 Promise 處理 多次 Ajax 請求(鏈式呼叫)【重要】
實際開發中,我們經常需要同時請求多個介面。比如說:在請求完介面1
的資料data1
之後,需要根據data1
的資料,繼續請求介面2,獲取data2
;然後根據data2
的資料,繼續請求介面3。
這種場景其實就是介面的多層巢狀呼叫。有了 promise之後,我們可以把多層巢狀呼叫按照線性的方式進行書寫,非常優雅。
也就是說:Promise 可以把原本的多層巢狀呼叫改進為鏈式呼叫。
程式碼舉例:(多次 Ajax請求,鏈式呼叫)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script type="text/javascript"> /* 基於Promise傳送Ajax請求 */ function queryData(url) { var promise = new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.readyState == 4 && xhr.status == 200) { // 處理正常情況 resolve(xhr.responseText); // xhr.responseText 是從介面拿到的資料 } else { // 處理異常情況 reject('介面請求失敗'); } }; xhr.responseType = 'json'; // 設定返回的資料型別 xhr.open('get', url); xhr.send(null); // 請求介面 }); return promise; } // 傳送多個ajax請求並且保證順序 queryData('http://localhost:3000/api1') .then( data1 => { console.log(JSON.stringify(data1)); // 請求完介面1後,繼續請求介面2 return queryData('http://localhost:3000/api2'); }, error1 => { console.log(error1); } ) .then( data2 => { console.log(JSON.stringify(data2)); // 請求完介面2後,繼續請求介面3 return queryData('http://localhost:3000/api3'); }, error2 => { console.log(error2); } ) .then( data3 => { // 獲取介面3返回的資料 console.log(JSON.stringify(data3)); }, error3 => { console.log(error3); } ); </script> </body> </html>
上面這個舉例很經典,需要多看幾遍。
return 的函式返回值
return 後面的返回值,有兩種情況:
-
情況1:返回 Promise 例項物件。返回的該例項物件會呼叫下一個 then。
-
情況2:返回普通值。返回的普通值會直接傳遞給下一個then,通過 then 引數中函式的引數接收該值。
我們針對上面這兩種情況,詳細解釋一下。
情況1:返回 Promise 例項物件
舉例如下:(這個例子,跟上一段 Ajax 鏈式呼叫 的例子差不多)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script type="text/javascript"> /* 基於Promise傳送Ajax請求 */ function queryData(url) { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.readyState == 4 && xhr.status == 200) { // 處理正常情況 resolve(xhr.responseText); } else { // 處理異常情況 reject('介面請求失敗'); } }; xhr.responseType = 'json'; // 設定返回的資料型別 xhr.open('get', url); xhr.send(null); // 請求介面 }); } // 傳送多個ajax請求並且保證順序 queryData('http://localhost:3000/api1') .then( data1 => { console.log(JSON.stringify(data1)); return queryData('http://localhost:3000/api2'); }, error1 => { console.log(error1); } ) .then( data2 => { console.log(JSON.stringify(data2)); // 這裡的 return,返回的是 Promise 例項物件 return new Promise((resolve, reject) => { resolve('qianguyihao'); }); }, error2 => { console.log(error2); } ) .then(data3 => { console.log(data3); }); </script> </body> </html>
情況2:返回 普通值
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script type="text/javascript"> /* 基於Promise傳送Ajax請求 */ function queryData(url) { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.readyState == 4 && xhr.status == 200) { // 處理正常情況 resolve(xhr.responseText); } else { // 處理異常情況 reject('介面請求失敗'); } }; xhr.responseType = 'json'; // 設定返回的資料型別 xhr.open('get', url); xhr.send(null); // 請求介面 }); } // 傳送多個ajax請求並且保證順序 queryData('http://localhost:3000/api1') .then( data1 => { console.log(JSON.stringify(data1)); return queryData('http://localhost:3000/api2'); }, error1 => { console.log(error1); } ) .then( data2 => { console.log(JSON.stringify(data2)); // 返回普通值 return 'qianguyihao'; }, error2 => { console.log(error2); } ) /* 既然上方返回的是 普通值,那麼,這裡的 then 是誰來呼叫呢? 答案是:這裡會產生一個新的 預設的 promise例項,來呼叫這裡的then,確保可以繼續進行鏈式操作。 */ .then(data3 => { // 這裡的 data3 接收的是 普通值 'qianguyihao' console.log(data3); }); </script> </body> </html>
Promise 的常用API:例項方法【重要】
Promise 自帶的API提供瞭如下例項方法:
-
promise.then():獲取非同步任務的正常結果。
-
promise.catch():獲取非同步任務的異常結果。
-
promise.finaly():非同步任務無論成功與否,都會執行。
程式碼舉例如下。
寫法1:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> function queryData() { return new Promise((resolve, reject) => { setTimeout(function() { var data = { retCode: 0, msg: 'qianguyihao' }; // 介面返回的資料 if (data.retCode == 0) { // 介面請求成功時呼叫 resolve(data); } else { // 介面請求失敗時呼叫 reject({ retCode: -1, msg: 'network error' }); } }, 100); }); } queryData() .then(data => { // 從 resolve 獲取正常結果 console.log('介面請求成功時,走這裡'); console.log(data); }) .catch(data => { // 從 reject 獲取異常結果 console.log('介面請求失敗時,走這裡'); console.log(data); }) .finally(() => { console.log('無論介面請求成功與否,都會走這裡'); }); </script> </body> </html>
寫法2:(和上面的寫法1等價)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> function queryData() { return new Promise((resolve, reject) => { setTimeout(function() { var data = { retCode: 0, msg: 'qianguyihao' }; // 介面返回的資料 if (data.retCode == 0) { // 介面請求成功時呼叫 resolve(data); } else { // 介面請求失敗時呼叫 reject({ retCode: -1, msg: 'network error' }); } }, 100); }); } queryData() .then( data => { // 從 resolve 獲取正常結果 console.log('介面請求成功時,走這裡'); console.log(data); }, data => { // 從 reject 獲取異常結果 console.log('介面請求失敗時,走這裡'); console.log(data); } ) .finally(() => { console.log('無論介面請求成功與否,都會走這裡'); }); </script> </body> </html>
注意:寫法1和寫法2的作用是完全等價的。只不過,寫法2是把 catch 裡面的程式碼作為 then裡面的第二個引數而已。
Promise 的常用API:物件方法【重要】
Promise 自帶的API提供瞭如下物件方法:
-
Promise.all():併發處理多個非同步任務,所有任務都執行成功,才能得到結果。
-
Promise.race(): 併發處理多個非同步任務,只要有一個任務執行成功,就能得到結果。
下面來詳細介紹。
Promise.all() 程式碼舉例
程式碼舉例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script type="text/javascript"> /* 封裝 Promise 介面呼叫 */ function queryData(url) { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.readyState == 4 && xhr.status == 200) { // 處理正常結果 resolve(xhr.responseText); } else { // 處理異常結果 reject('伺服器錯誤'); } }; xhr.open('get', url); xhr.send(null); }); } var promise1 = queryData('http://localhost:3000/a1'); var promise2 = queryData('http://localhost:3000/a2'); var promise3 = queryData('http://localhost:3000/a3'); Promise.all([promise1, promise2, promise3]).then(result => { console.log(result); }); </script> </body> </html>
Promise.race() 程式碼舉例
程式碼舉例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script type="text/javascript"> /* 封裝 Promise 介面呼叫 */ function queryData(url) { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.readyState == 4 && xhr.status == 200) { // 處理正常結果 resolve(xhr.responseText); } else { // 處理異常結果 reject('伺服器錯誤'); } }; xhr.open('get', url); xhr.send(null); }); } var promise1 = queryData('http://localhost:3000/a1'); var promise2 = queryData('http://localhost:3000/a2'); var promise3 = queryData('http://localhost:3000/a3'); Promise.race([promise1, promise2, promise3]).then(result => { console.log(result); }); </script> </body> </html>