0%

每天一點點,學習ES6-七-Promise

前言

今天來研究 Promise

很多大哥學會 **Promise**後都覺得不過如此
但初次接觸 我完全看不懂啊~~~~~

懊惱

在以往我們如果處理多層異步操作,我們往往會像下面那樣編寫我們的代碼

1
2
3
4
5
6
7
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('得到最终结果: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);

閱讀上面代碼,是不是很難受,上述形成了經典的回調地獄

現在通過Promise的改寫上面的代碼

1
2
3
4
5
6
7
8
9
10
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('得到最终结果: ' + finalResult);
})
.catch(failureCallback);

一個 promise 代表一個異步運算的最終結果

Promise 語法結構提供了更多的程式設計上的可能性,它是一個經過長時間實戰的結構,在許多知名的函式庫或框架中很早就有見到 Promise 物件的身影,例如 Dojo、jQuery、YUI、 Ember、Angular、WinJS、Q 等等,之後 Promises/A+ 社區則提供了統一的標準。在最近新一代的 ES6 標準中將會包含了 Promise 的實作,提供原生的語言內建支援,這將是個開始,往後會有愈來愈多 API 以此為基礎架構在其上

Promise 這種異步執行結構的需求,在伺服器端(Node.js)遠遠大於瀏覽器端,原因是瀏覽器通常只有會有一個使用者在操作,而且除了網路連線要求之類的 I/O(例如 AJAX)、DOM 事件處理、動畫流程處理(計時器)外,並沒有太多需要異步執行 I/O 處理的情況。伺服器端(Node.js)所面臨到情況就很嚴峻,除了與外部資源 I/O 的處理情況到處都有之外,而且伺服器端(Node.js)是需要同時服務多人使用的情況。

Promise 是一個強大的異步執行流程語法結構,在 ES6 Promise 標準中,實作內容只有一個建構函式與一個then方法、一個catch方法,再加上四個必需以Promise關鍵字呼叫的靜態函式,Promise.resolvePromise.rejectPromise.allPromise.race。語法介紹頁面只有 7 頁,內容少之又少。但為何 Promise 結構不容易被理解?原因在於同步與異步回調函式執行的概念,以及其中很多流程運作,需要用另一種方式來思考要如何進行。當然,需要理解的規則也很多。

以初學者來說,從規則來理解它的運作方式,會是比較容易進入的學習路徑。


說道 Promise 前要先了解 異步 Callback(回調)

異步 Callback(回調)

Promise 中的所有回調函式,都是異步執行的

但 並非所有使用Callback Function 的API 都是異步執行的!!!!

JS語言內建 API 中使用的回調函式不一定是異步執行的,也有同步執行
例如 Array.forEach

要讓開發者自訂的 callbacks(回調)的執行轉變為異步,有以下幾種方式:

  • 使用計時器(timer)函式: setTimeout, setInterval
  • 特殊的函式: nextTick, setImmediate
  • 執行 I/O: 監聽網路、資料庫查詢或讀寫外部資源
  • 訂閱事件

註: 執行 I/O 的 API 通常會出現在伺服器端(Node.js),例如讀寫檔案、資料庫互動等等,這些 API 都會經過特別的設計。瀏覽器端只有少數幾個。

Promise結構中異步回調函式只是其中一個重要的參與分子,但 Promise 的重點並不只是在異步執行的回調函式,它可以把多個異步執行的函式,執行流程轉變為序列執行(一個接一個),或是並行執行(全部都要處理完再說),並且作更好的錯誤處理方式。也就是說,Promise 結構是一種異步執行的控制流程架構


異步 Function 與 同步 Function

同步 Function

同步執行函式的結果要不就是回傳一個值,要不然就是執行到一半發生例外,中斷目前的程式然後拋出例外。

異步 Function

異步執行函式的結果要不就是帶有回傳值的成功,要不就是帶有回傳理由的失敗。

兩個看起來很像

可以理解成 ↓

  • 同步,程式 發生錯誤就會停止
  • 異步,程式 發生錯誤還是會繼續,只是會給你一個錯誤的理由

Promise 物件的設計就是針對異步函式的執行結果所設計的,promise 物件最後的結果要不然就用一個回傳值來 fulfilled(實現),要不然就用一個理由(錯誤)來 rejected(拒絕)。

你可能會認為這種用失敗(或拒絕)或成功的兩分法結果,似乎有點太武斷了,但在許多異步的結構中,的確是用成功或失敗來作為代表,例如 AJAX 的語法結構。promise 物件用實現(解決)與拒絕來作為兩分法的分別字詞。對於有回傳值的情況,沒有什麼太多的考慮空間,必定都是實現狀態,但對於何時才算是拒絕的狀態,這有可能需要仔細考量,例如以下的情況:

  • 好的拒絕狀態應該是:

    • I/O 操作時發生錯誤,例如讀寫檔案或是網路上的資料時,中途發生例外情況
    • 無法完成預期的工作,例如accessUsersContacts函式是要讀取手機上的聯絡人名單,因為權限不足而失敗
    • 內部錯誤導致無法進行異步的程序,例如環境的問題或是程式開發者傳送錯誤的傳入值
  • 壞的拒絕狀態例如:

    • 沒有找到值或是輸出是空白的情況,例如對資料庫查詢,目前沒有找到結果,回傳值是 0。它不應該是個拒絕狀態,而是帶有 0 值的實現。
    • 詢問類的函式,例如hasPermissionToAccessUsersContacts函式詢問是否有讀取手機上聯絡人名單的權限,當回傳的結果是 false,也就是沒有權限時,應該是一個帶有 false 值的實現。

不同的想法會導致不同的設計,舉一個明確的實例來說明拒絕狀態的情境設計。

jQueryajax()方法,它在失敗時會呼叫fail處理函式

失敗的情況除了網路連線的問題外,它會在雖然伺服器有回應,
但是是屬於失敗類型的 HTTP 狀態碼時,也算作是失敗的狀態。

但另一個可以用於類似功能的 Fetch API 並沒有,fetch使用 Promise 架構,只有在網路連線發生問題才會轉為 rejected(拒絕)狀態,只要是伺服器有回應都算已實現狀態。

註: 在 JavaScript 中函式的設計,必定有回傳值,沒寫只是回傳 undefined,相當於return undefined


Promise 狀態

promise對象僅有三種狀態

  • pending(進行中)
  • fulfilled(已成功)
  • rejected(已失敗)

Promise 特點

  • 對象的狀態不受外界影響,只有異步操作的結果,可以決定當前是哪一種狀態
  • 一旦狀態改變(從pending變為fulfilled 和 從pending變為rejected),
    就不會再變,任何時候都可以得到這個結果

Promise 流程

認真閱讀下圖,我們能夠輕鬆了解promise整個流程

promise流程

Promise 用法

Promise對像是一個構造函數,用來生成Promise實例

1
const promise = new Promise(function(resolve, reject) {});

Promise 代碼結構

  • 在通過new創建Promise對象時,我們需要傳入一個回調函數,我們稱之為executor;
    • 這個回調函數會被立即執行,並且給傳入另外兩個回調函數resolve、reject;
    • 當我們調用resolve回調函數時,會執行Promise對象的then方法傳入的回調函數;
    • 當我們調用reject回調函數時,會執行Promise對象的catch方法傳入的回調函數;

下面Promise使用過程,我們可以將它劃分成三個狀態:

  • 待定(pending): 初始狀態,既沒有被兌現,也沒有被拒絕;

    • 當執行executor中的代碼時,處於該狀態;
  • 已兌現(fulfilled): 意味著操作成功完成;

    • 執行了resolve時,處於該狀態;
  • 已拒絕(rejected): 意味著操作失敗;

    • 執行了reject時,處於該狀態;
待定(pending) 已兌現(fulfilled) 已拒絕(rejected)
初始狀態,既沒有被兌現,也沒有被拒絕 操作成功完成 操作失敗
當執行executor中的代碼時,
處於該狀態
執行了resolve時,
處於該狀態
執行了reject時,
處於該狀態

我們來看一下Promise代碼結構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function foo() {
  // Promise
  return new Promise((resolve, reject) => {
    resolve("success message")
    // reject("failture message")
  })
}

const fooPromise = foo()
// then方法傳入的回調函數兩個回掉函數:
// > 第一個, 會在Promise執行resolve函數時, 被回調
// > 第二個, 會在Promise執行reject函數時, 被回調

fooPromise.then((res) => {
  console.log(res)
}, (err) => {
  console.log(err)
})

//catch方法傳入的回調函數, 會在Promise執行reject函數時, 被回調

fooPromise.catch(() => {
})


// 傳入的這個函數, 被稱之為 executor
// > resolve: 回調函數, 在成功時, 回調resolve函數
// > reject: 回調函數, 在失敗時, 回調reject函數

const promise = new Promise((resolve, reject) => {
console.log("promise傳入的函數被執行了")
  // resolve()
reject()
})

promise.then(() => {
})

promise.catch(() => {
})

Promise重構請求

那麼有了Promise,我們就可以將之前的代碼進行重構了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// request.js
function requestData(url) {
// 異步請求的代碼會被放入到executor中
return new Promise((resolve, reject) => {
// 模擬網路請求
setTimeout(() => {
// 拿到請求的结果
// url傳入的是yz, 請求成功
if (url === "yz") {
// 成功
let names = ["abc", "cba", "nba"]
resolve(names)
} else { // 否則請求失敗
// 失敗
let errMessage = "請求失敗, url錯誤"
reject(errMessage)
}
}, 3000);
})
}

// main.js
const promise = requestData("coderWhy")
promise.then((res) => {
console.log("請求成功:", res)
}, (err) => {
console.log("請求失敗:", err)
})

今天先研究到這邊

明天再來研究實例方法


References

你的支持是我活力的來源 :)