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>  

了解這些內容之後, Promise 的基本用法,你就已經掌握了。