本章節是關於非同步程式設計的介紹,特別是透過 Promises 和 ECMAScript 6 Promise API。 上一章說明了 JavaScript 中非同步程式設計的基礎。當您在本章中遇到任何不理解的地方時,可以參閱它。
then()
呼叫fs.readFile()
承諾化
XMLHttpRequest
承諾化
Promise.resolve()
Promise.reject()
onRejected
解析 Q
then()
進行錯誤處理Promise.all()
分岔和合併運算
Promise.all()
的 map()
Promise.race()
的計時
done()
finally()
Promise
建構函數Promise
方法Promise.prototype
方法Promise 是提供非同步運算結果的另一種選擇,取代回呼。非同步函數的實作者需要付出更多努力,但這些函數的使用者可以獲得許多好處。
下列函數透過 Promise 非同步傳回結果
function
asyncFunc
()
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
···
resolve
(
result
);
···
reject
(
error
);
});
}
您呼叫 asyncFunc()
的方式如下
asyncFunc
()
.
then
(
result
=>
{
···
})
.
catch
(
error
=>
{
···
});
then()
呼叫 then()
始終傳回 Promise,讓您可以串接方法呼叫
asyncFunc1
()
.
then
(
result1
=>
{
// Use result1
return
asyncFunction2
();
// (A)
})
.
then
(
result2
=>
{
// (B)
// Use result2
})
.
catch
(
error
=>
{
// Handle errors of asyncFunc1() and asyncFunc2()
});
then()
傳回的 Promise P 如何解決,取決於其回呼執行的動作
asyncFunction2
的 Promise 的解決。此外,請注意 catch()
如何處理兩個非同步函數呼叫(asyncFunction1()
和 asyncFunction2()
)的錯誤。也就是說,未捕捉的錯誤會持續傳遞,直到出現錯誤處理常式。
如果您透過 then()
串接非同步函數呼叫,它們會依序執行,一次一個
asyncFunc1
()
.
then
(()
=>
asyncFunc2
());
如果你不這樣做,而是立即呼叫它們,它們基本上會並行執行(在 Unix 程序術語中稱為 fork)
asyncFunc1
();
asyncFunc2
();
Promise.all()
讓你可以在所有結果都出來後收到通知(在 Unix 程序術語中稱為 join)。它的輸入是 Promise 陣列,它的輸出是單一 Promise,並以結果陣列來完成。
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
])
.
then
(([
result1
,
result2
])
=>
{
···
})
.
catch
(
err
=>
{
// Receives first rejection among the Promises
···
});
Promise API 關於非同步傳遞結果。Promise 物件(簡稱:Promise)是結果的替身,透過該物件傳遞。
狀態
對狀態變更做出反應
then()
註冊的回呼,用於接收完成或拒絕的通知。then()
方法的物件。每當 API 僅有興趣接收 settled 的通知時,它只要求 thenable(例如從 then()
和 catch()
傳回的值;或傳遞給 Promise.all()
和 Promise.race()
的值)。變更狀態:有兩個變更 Promise 狀態的操作。在呼叫其中一個操作一次後,後續呼叫不會產生任何效果。
Promises 是一種模式,有助於一種特定類型的非同步程式設計:非同步傳回單一結果的函式(或方法)。接收此類結果的一種流行方式是透過回呼(「回呼作為延續」)
asyncFunction
(
arg1
,
arg2
,
result
=>
{
console
.
log
(
result
);
});
承諾提供更好的方式來處理回呼:現在非同步函數會傳回一個承諾,這是一個物件,用作最終結果的佔位符和容器。透過承諾方法 then()
註冊的回呼會收到結果的通知
asyncFunction
(
arg1
,
arg2
)
.
then
(
result
=>
{
console
.
log
(
result
);
});
與作為延續的回呼相比,承諾具有以下優點
then()
的回呼傳回一個承諾(例如呼叫另一個基於承諾的函數的結果),則 then()
會傳回該承諾(這實際上是如何運作的,後面會說明得更複雜)。因此,您可以串連 then()
方法呼叫
asyncFunction1
(
a
,
b
)
.
then
(
result1
=>
{
console
.
log
(
result1
);
return
asyncFunction2
(
x
,
y
);
})
.
then
(
result2
=>
{
console
.
log
(
result2
);
});
讓我們來看一個第一個範例,讓您了解使用承諾的感覺。
使用 Node.js 風格的回呼,非同步讀取檔案看起來像這樣
fs
.
readFile
(
'config.json'
,
function
(
error
,
text
)
{
if
(
error
)
{
console
.
error
(
'Error while reading config file'
);
}
else
{
try
{
const
obj
=
JSON
.
parse
(
text
);
console
.
log
(
JSON
.
stringify
(
obj
,
null
,
4
));
}
catch
(
e
)
{
console
.
error
(
'Invalid JSON in file'
);
}
}
});
使用承諾,相同的功能會像這樣使用
readFilePromisified
(
'config.json'
)
.
then
(
function
(
text
)
{
// (A)
const
obj
=
JSON
.
parse
(
text
);
console
.
log
(
JSON
.
stringify
(
obj
,
null
,
4
));
})
.
catch
(
function
(
error
)
{
// (B)
// File read error or JSON SyntaxError
console
.
error
(
'An error occurred'
,
error
);
});
仍然有回呼,但它們是透過在結果上呼叫的方法提供的(then()
和 catch()
)。B 行中的錯誤回呼在兩個方面很方便:首先,它是一種處理錯誤的單一風格(與前一個範例中的 if (error)
和 try-catch
相比)。其次,您可以從單一位置處理 readFilePromisified()
和 A 行中回呼的錯誤。
readFilePromisified()
的程式碼 稍後會說明。
讓我們來看了解承諾的三種方式。
以下程式碼包含一個基於承諾的函數 asyncFunc()
及其呼叫。
function
asyncFunc
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
// (A)
setTimeout
(()
=>
resolve
(
'DONE'
),
100
);
// (B)
});
}
asyncFunc
()
.
then
(
x
=>
console
.
log
(
'Result: '
+
x
));
// Output:
// Result: DONE
asyncFunc()
傳回 Promise。一旦非同步運算的實際結果 'DONE'
準備好,它會透過 resolve()
(B 行) 傳送,而 resolve()
是 A 行開始的回呼函式的參數。
那麼 Promise 是什麼?
asyncFunc()
是封鎖函式呼叫。以下程式碼從非同步函式 main()
呼叫 asyncFunc()
。 非同步函式 是 ECMAScript 2017 的功能。
async
function
main
()
{
const
x
=
await
asyncFunc
();
// (A)
console
.
log
(
'Result: '
+
x
);
// (B)
// Same as:
// asyncFunc()
// .then(x => console.log('Result: '+x));
}
main
();
main()
的主體清楚地表達了概念上發生了什麼事,我們通常如何思考非同步運算。也就是說,asyncFunc()
是封鎖函式呼叫
asyncFunc()
完成。x
。在 ECMAScript 6 和產生器之前,您無法暫停和繼續程式碼。這就是為什麼對於 Promise,您將程式碼繼續之後發生的一切放入回呼函式的原因。呼叫該回呼函式與繼續程式碼相同。
如果函式傳回 Promise,則該 Promise 就如同一個空白,函式會在計算出結果後(通常)填入結果。您可以透過陣列模擬此程序的簡化版本
function
asyncFunc
()
{
const
blank
=
[];
setTimeout
(()
=>
blank
.
push
(
'DONE'
),
100
);
return
blank
;
}
const
blank
=
asyncFunc
();
// Wait until the value has been filled in
setTimeout
(()
=>
{
const
x
=
blank
[
0
];
// (A)
console
.
log
(
'Result: '
+
x
);
},
200
);
使用 Promise 時,您不會透過 [0]
(如 A 行所示)存取最終值,而是使用 then()
方法和回呼函式。
檢視 Promise 的另一種方式是將它視為發射事件的物件。
function
asyncFunc
()
{
const
eventEmitter
=
{
success
:
[]
};
setTimeout
(()
=>
{
// (A)
for
(
const
handler
of
eventEmitter
.
success
)
{
handler
(
'DONE'
);
}
},
100
);
return
eventEmitter
;
}
asyncFunc
()
.
success
.
push
(
x
=>
console
.
log
(
'Result: '
+
x
));
// (B)
註冊事件監聽器(第 B 行)可以在呼叫 asyncFunc()
之後執行,因為傳遞給 setTimeout()
的回呼函式(第 A 行)會非同步執行(在此段程式碼完成之後)。
一般事件發射器專門用於傳送多個事件,在您註冊後立即開始。
相對地,Promise 專門用於傳送一個值,並內建防範太晚註冊的保護機制:Promise 的結果會快取,並傳遞給在 Promise 解決後註冊的事件監聽器。
讓我們看看 Promise 如何從生產者和消費者端操作。
作為生產者,您建立一個 Promise 並透過它傳送結果
const
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
// (A)
···
if
(
···
)
{
resolve
(
value
);
// success
}
else
{
reject
(
reason
);
// failure
}
});
透過 Promise 傳送結果後,Promise 會鎖定在該結果。這表示每個 Promise 始終處於三個(互斥)狀態之一
如果 Promise 已完成或已拒絕,則該 Promise 會解決(它所代表的計算已完成)。Promise 只會解決一次,然後保持已解決狀態。後續的解決嘗試不會產生任何效果。
new Promise()
的參數(從第 A 行開始)稱為執行器
resolve()
傳送結果。這通常會完成 Promise p
。但可能不會 - 使用 Promise q
解決會導致 p
追蹤 q
:如果 q
仍待處理,則 p
也是如此。但是 q
已解決,p
也會以相同方式解決。reject()
通知 Promise 消費者。這總是會拒絕 Promise。如果在執行器內部引發例外狀況,p
會以該例外狀況拒絕。
作為 promise
的使用者,您會透過反應(您使用 then()
和 catch()
方法註冊的回呼函式)收到完成或拒絕的通知
promise
.
then
(
value
=>
{
/* fulfillment */
})
.
catch
(
error
=>
{
/* rejection */
});
承諾對非同步函數(一次性結果)如此有用的原因,在於一旦承諾已解決,它就不會再改變。此外,永遠不會有任何競爭條件,因為無論您在承諾解決之前或之後呼叫 then()
或 catch()
都無所謂
請注意,catch()
只是呼叫 then()
的一個更方便(且建議)的替代方案。也就是說,以下兩個呼叫是等效的
promise
.
then
(
null
,
error
=>
{
/* rejection */
});
promise
.
catch
(
error
=>
{
/* rejection */
});
承諾函式庫可以完全控制是否同步(立即)或非同步(在當前延續、當前程式碼片段完成後)將結果傳遞給承諾反應。然而,Promises/A+ 規範要求總是使用後一種執行模式。它透過以下 then()
方法的 需求 (2.2.4) 說明這一點
onFulfilled
或onRejected
必須等到執行內容堆疊僅包含平台程式碼時才能呼叫。
這表示您的程式碼可以依賴執行完成語意(如 前一章 所述),而且連鎖承諾不會讓其他任務缺乏處理時間。
此外,此限制可防止您撰寫有時立即傳回結果、有時非同步傳回結果的函數。這是一種反模式,因為它會讓程式碼難以預測。如需更多資訊,請參閱 Isaac Z. Schlueter 的「為非同步設計 API」。
在我們深入探討承諾之前,讓我們在幾個範例中使用我們到目前為止所學到的內容。
fs.readFile()
承諾化 以下程式碼是內建 Node.js 函數 fs.readFile()
的基於承諾的版本。
import
{
readFile
}
from
'fs'
;
function
readFilePromisified
(
filename
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
readFile
(
filename
,
{
encoding
:
'utf8'
},
(
error
,
data
)
=>
{
if
(
error
)
{
reject
(
error
);
}
else
{
resolve
(
data
);
}
});
});
}
readFilePromisified()
的用法如下
readFilePromisified
(
process
.
argv
[
2
])
.
then
(
text
=>
{
console
.
log
(
text
);
})
.
catch
(
error
=>
{
console
.
log
(
error
);
});
XMLHttpRequest
轉為 Promise 以下是一個基於 Promise 的函式,它透過基於事件的 XMLHttpRequest API 執行 HTTP GET
function
httpGet
(
url
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
const
request
=
new
XMLHttpRequest
();
request
.
onload
=
function
()
{
if
(
this
.
status
===
200
)
{
// Success
resolve
(
this
.
response
);
}
else
{
// Something went wrong (404 etc.)
reject
(
new
Error
(
this
.
statusText
));
}
};
request
.
onerror
=
function
()
{
reject
(
new
Error
(
'XMLHttpRequest Error: '
+
this
.
statusText
));
};
request
.
open
(
'GET'
,
url
);
request
.
send
();
});
}
以下是 httpGet()
的使用方法
httpGet
(
'http://example.com/file.txt'
)
.
then
(
function
(
value
)
{
console
.
log
(
'Contents: '
+
value
);
},
function
(
reason
)
{
console
.
error
(
'Something went wrong'
,
reason
);
});
我們將 setTimeout()
實作為基於 Promise 的函式 delay()
(類似於 Q.delay()
)。
function
delay
(
ms
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
resolve
,
ms
);
// (A)
});
}
// Using delay():
delay
(
5000
).
then
(
function
()
{
// (B)
console
.
log
(
'5 seconds have passed!'
)
});
請注意,在 A 行中,我們呼叫 resolve
而沒有參數,這等同於呼叫 resolve(undefined)
。我們也不需要 B 行中的完成值,因此只需忽略它。在此,只要收到通知就已足夠。
function
timeout
(
ms
,
promise
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
promise
.
then
(
resolve
);
setTimeout
(
function
()
{
reject
(
new
Error
(
'Timeout after '
+
ms
+
' ms'
));
// (A)
},
ms
);
});
}
請注意,逾時後的拒絕(在 A 行中)不會取消要求,但會阻止 Promise 以其結果完成。
以下是 timeout()
的使用方法
timeout
(
5000
,
httpGet
(
'http://example.com/file.txt'
))
.
then
(
function
(
value
)
{
console
.
log
(
'Contents: '
+
value
);
})
.
catch
(
function
(
reason
)
{
console
.
error
(
'Error or timeout'
,
reason
);
});
現在我們準備深入探討 Promise 的功能。我們先來探討建立 Promise 的另外兩種方式。
Promise.resolve()
Promise.resolve(x)
的運作方式如下
x
,它會傳回一個以 x
完成的 Promise
Promise
.
resolve
(
'abc'
)
.
then
(
x
=>
console
.
log
(
x
));
// abc
x
是建構函式為接收器(如果您呼叫 Promise.resolve()
則為 Promise
)的 Promise,則 x
會不變地傳回
const
p
=
new
Promise
(()
=>
null
);
console
.
log
(
Promise
.
resolve
(
p
)
===
p
);
// true
x
是 thenable,則會將它轉換為 Promise:thenable 的解決也會成為 Promise 的解決。以下程式碼示範了這一點。fulfilledThenable
的行為大致上就像一個以字串 'hello'
完成的 Promise。將它轉換為 Promise promise
之後,方法 then()
會如預期般運作(最後一行)。
const
fulfilledThenable
=
{
then
(
reaction
)
{
reaction
(
'hello'
);
}
};
const
promise
=
Promise
.
resolve
(
fulfilledThenable
);
console
.
log
(
promise
instanceof
Promise
);
// true
promise
.
then
(
x
=>
console
.
log
(
x
));
// hello
這表示您可以使用 Promise.resolve()
將任何值(Promise、thenable 或其他)轉換為 Promise。事實上,Promise.all()
和 Promise.race()
會使用它將任意值的陣列轉換為 Promise 的陣列。
Promise.reject()
Promise.reject(err)
會傳回一個以 err
拒絕的 Promise
const
myError
=
new
Error
(
'Problem!'
);
Promise
.
reject
(
myError
)
.
catch
(
err
=>
console
.
log
(
err
===
myError
));
// true
在本節中,我們將仔細探討如何串連 Promise。方法呼叫的結果
P
.
then
(
onFulfilled
,
onRejected
)
是一個新的 Promise Q。這表示您可以透過呼叫 Q 的 then()
來維持基於 Promise 的控制流程
onFulfilled
或 onRejected
傳回的內容來解析。onFulfilled
或 onRejected
擲回例外狀況,則 Q 會被拒絕。如果您使用一般值解析 then()
傳回的 Promise Q,您可以透過後續的 then()
來取得該值
asyncFunc
()
.
then
(
function
(
value1
)
{
return
123
;
})
.
then
(
function
(
value2
)
{
console
.
log
(
value2
);
// 123
});
您也可以使用 thenable R 來解析 then()
傳回的 Promise Q。thenable 是任何具有方法 then()
的物件,其運作方式類似於 Promise.prototype.then()
。因此,Promise 是 thenable。使用 R 解析(例如,從 onFulfilled
傳回 R)表示它會插入在 Q「之後」:R 的結算會轉發到 Q 的 onFulfilled
和 onRejected
回呼。在某種程度上,Q 會變成 R。
此機制的用途主要是扁平化嵌套的 then()
呼叫,如下列範例所示
asyncFunc1
()
.
then
(
function
(
value1
)
{
asyncFunc2
()
.
then
(
function
(
value2
)
{
···
});
})
扁平化版本如下所示
asyncFunc1
()
.
then
(
function
(
value1
)
{
return
asyncFunc2
();
})
.
then
(
function
(
value2
)
{
···
})
onRejected
解析 Q 您在錯誤處理常式中傳回的任何內容都會變成履行值(而不是拒絕值!)。這允許您指定在失敗時使用的預設值
retrieveFileName
()
.
catch
(
function
()
{
// Something went wrong, use a default value
return
'Untitled.txt'
;
})
.
then
(
function
(
fileName
)
{
···
});
在 then()
和 catch()
的回呼中擲回的例外狀況會作為拒絕傳遞到下一個錯誤處理常式
asyncFunc
()
.
then
(
function
(
value
)
{
throw
new
Error
();
})
.
catch
(
function
(
reason
)
{
// Handle error here
});
可能會有一個或多個沒有錯誤處理常式的 then()
方法呼叫。然後,錯誤會一直傳遞,直到出現錯誤處理常式為止。
asyncFunc1
()
.
then
(
asyncFunc2
)
.
then
(
asyncFunc3
)
.
catch
(
function
(
reason
)
{
// Something went wrong above
});
在下列程式碼中,建立了兩個 Promise 的鏈,但只傳回了鏈的第一部分。因此,鏈的尾端遺失了。
// Don’t do this
function
foo
()
{
const
promise
=
asyncFunc
();
promise
.
then
(
result
=>
{
···
});
return
promise
;
}
這可以透過傳回鏈的尾端來修正
function
foo
()
{
const
promise
=
asyncFunc
();
return
promise
.
then
(
result
=>
{
···
});
}
如果你不需要變數 promise
,你可以進一步簡化這段程式碼
function
foo
()
{
return
asyncFunc
()
.
then
(
result
=>
{
···
});
}
在以下程式碼中,asyncFunc2()
的呼叫是巢狀的
// Don’t do this
asyncFunc1
()
.
then
(
result1
=>
{
asyncFunc2
()
.
then
(
result2
=>
{
···
});
});
修正方法是取消巢狀,透過傳回第一個 then()
的第二個 Promise,並透過第二個鏈結的 then()
來處理
asyncFunc1
()
.
then
(
result1
=>
{
return
asyncFunc2
();
})
.
then
(
result2
=>
{
···
});
在以下程式碼中,方法 insertInto()
為其結果建立一個新的 Promise(第 A 行)
// Don’t do this
class
Model
{
insertInto
(
db
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
// (A)
db
.
insert
(
this
.
fields
)
// (B)
.
then
(
resultCode
=>
{
this
.
notifyObservers
({
event
:
'created'
,
model
:
this
});
resolve
(
resultCode
);
// (C)
}).
catch
(
err
=>
{
reject
(
err
);
// (D)
})
});
}
···
}
如果你仔細觀察,你可以看到結果 Promise 主要用於轉發非同步方法呼叫 db.insert()
(第 B 行)的達成(第 C 行)和拒絕(第 D 行)。
修正方法是不建立 Promise,而是依賴 then()
和鏈結
class
Model
{
insertInto
(
db
)
{
return
db
.
insert
(
this
.
fields
)
// (A)
.
then
(
resultCode
=>
{
this
.
notifyObservers
({
event
:
'created'
,
model
:
this
});
return
resultCode
;
// (B)
});
}
···
}
說明
resultCode
(第 B 行),並讓 then()
為我們建立 Promise。then()
會傳遞 db.insert()
產生的任何拒絕。then()
進行錯誤處理 原則上,catch(cb)
是 then(null, cb)
的縮寫。但同時使用 then()
的兩個參數可能會造成問題
// Don’t do this
asyncFunc1
()
.
then
(
value
=>
{
// (A)
doSomething
();
// (B)
return
asyncFunc2
();
// (C)
},
error
=>
{
// (D)
···
});
拒絕回呼(第 D 行)會接收 asyncFunc1()
的所有拒絕,但它不會接收達成回呼(第 A 行)建立的拒絕。例如,第 B 行的同步函式呼叫可能會擲回例外,或第 C 行的非同步函式呼叫可能會產生拒絕。
因此,最好將拒絕回呼移到鏈結的 catch()
asyncFunc1
()
.
then
(
value
=>
{
doSomething
();
return
asyncFunc2
();
})
.
catch
(
error
=>
{
···
});
在程式中,有兩種錯誤
對於操作錯誤,每個函數都應該支援一種錯誤訊號傳遞方式。對於基於 Promise 的函數,這表示不要混用拒絕和例外,這等同於說它們不應該拋出例外。
對於程式設計師錯誤,透過拋出例外,盡快失敗是有意義的
function
downloadFile
(
url
)
{
if
(
typeof
url
!==
'string'
)
{
throw
new
Error
(
'Illegal argument: '
+
url
);
}
return
new
Promise
(
···
).
}
如果您這樣做,您必須確保您的非同步程式碼可以處理例外。我認為對於理論上可以靜態檢查的斷言和類似事項(例如,透過分析原始碼的 linter)拋出例外是可以接受的。
如果在 then()
和 catch()
的回呼函式中拋出例外,這不是問題,因為這兩個方法會將它們轉換為拒絕。
但是,如果您透過執行同步操作來啟動非同步函數,情況就不同了
function
asyncFunc
()
{
doSomethingSync
();
// (A)
return
doSomethingAsync
()
.
then
(
result
=>
{
···
});
}
如果在 A 行拋出例外,整個函數就會拋出例外。這個問題有兩個解決方案。
您可以捕捉例外並將它們作為被拒絕的 Promise 傳回
function
asyncFunc
()
{
try
{
doSomethingSync
();
return
doSomethingAsync
()
.
then
(
result
=>
{
···
});
}
catch
(
err
)
{
return
Promise
.
reject
(
err
);
}
}
您也可以透過 Promise.resolve()
啟動 then()
方法呼叫的鏈,並在回呼函式中執行同步程式碼
function
asyncFunc
()
{
return
Promise
.
resolve
()
.
then
(()
=>
{
doSomethingSync
();
return
doSomethingAsync
();
})
.
then
(
result
=>
{
···
});
}
另一種方法是透過 Promise 建構函式啟動 Promise 鏈
function
asyncFunc
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
doSomethingSync
();
resolve
(
doSomethingAsync
());
})
.
then
(
result
=>
{
···
});
}
這種方法可以節省一個 tick(同步程式碼會立即執行),但會讓您的程式碼不那麼有規律。
本節的來源
組合是指利用現有部分建立新事物。我們已經接觸過 Promise 的順序組合:給定兩個 Promise P 和 Q,以下程式碼會產生一個新的 Promise,在 P 完成後執行 Q。
P
.
then
(()
=>
Q
)
請注意,這與同步程式碼的分號類似:同步作業 f()
和 g()
的順序組合如下所示。
f
();
g
()
本節說明組合 Promise 的其他方式。
假設您想要平行執行兩個非同步運算,asyncFunc1()
和 asyncFunc2()
// Don’t do this
asyncFunc1
()
.
then
(
result1
=>
{
handleSuccess
({
result1
});
});
.
catch
(
handleError
);
asyncFunc2
()
.
then
(
result2
=>
{
handleSuccess
({
result2
});
})
.
catch
(
handleError
);
const
results
=
{};
function
handleSuccess
(
props
)
{
Object
.
assign
(
results
,
props
);
if
(
Object
.
keys
(
results
).
length
===
2
)
{
const
{
result1
,
result2
}
=
results
;
···
}
}
let
errorCounter
=
0
;
function
handleError
(
err
)
{
errorCounter
++
;
if
(
errorCounter
===
1
)
{
// One error means that everything failed,
// only react to first error
···
}
}
兩個函式呼叫 asyncFunc1()
和 asyncFunc2()
沒有 then()
串接。因此,它們會立即執行,而且或多或少平行執行。執行現在分岔;每個函式呼叫產生一個獨立的「執行緒」。一旦兩個執行緒都完成(有結果或錯誤),執行會合併到 handleSuccess()
或 handleError()
中的單一執行緒。
這種方法的問題在於它涉及太多手動且容易出錯的工作。解決方法是不要自己執行,而是依賴內建方法 Promise.all()
。
Promise.all()
分岔和合併運算 Promise.all(iterable)
會接受一個 Promise 的 iterable(thenables 和其他值會透過 Promise.resolve()
轉換為 Promise)。一旦全部完成,它會使用其值的陣列來完成。如果 iterable
為空,all()
傳回的 Promise 會立即完成。
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
])
.
then
(([
result1
,
result2
])
=>
{
···
})
.
catch
(
err
=>
{
// Receives first rejection among the Promises
···
});
Promise.all()
的 map()
Promise 的一個好處是許多同步工具仍然有效,因為基於 Promise 的函式會傳回結果。例如,您可以使用陣列方法 map()
const
fileUrls
=
[
'http://example.com/file1.txt'
,
'http://example.com/file2.txt'
,
];
const
promisedTexts
=
fileUrls
.
map
(
httpGet
);
promisedTexts
是 Promise 的陣列。我們可以使用前一節中已經接觸過的 Promise.all()
,將該陣列轉換為使用結果陣列完成的 Promise。
Promise
.
all
(
promisedTexts
)
.
then
(
texts
=>
{
for
(
const
text
of
texts
)
{
console
.
log
(
text
);
}
})
.
catch
(
reason
=>
{
// Receives first rejection among the Promises
});
Promise.race()
設定逾時 Promise.race(iterable)
會接收一個 Promise 的 iterable(thenables 和其他值會透過 Promise.resolve()
轉換為 Promise),並傳回一個 Promise P。第一個已解決的輸入 Promise 會將其解決狀態傳遞給輸出 Promise。如果 iterable
為空,則 race()
傳回的 Promise 永遠不會解決。
舉例來說,我們可以使用 Promise.race()
來實作逾時
Promise
.
race
([
httpGet
(
'http://example.com/file.txt'
),
delay
(
5000
).
then
(
function
()
{
throw
new
Error
(
'Timed out'
)
});
])
.
then
(
function
(
text
)
{
···
})
.
catch
(
function
(
reason
)
{
···
});
本節說明 Promise 的兩個有用方法,許多 Promise 函式庫都提供這些方法。它們只會用來進一步示範 Promise,你不應該將它們新增到 Promise.prototype
(這種修補只應該由多重填補來執行)。
done()
當你串連多個 Promise 方法呼叫時,你可能會在不知不覺中捨棄錯誤。例如
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
then
(
f2
);
// (A)
}
如果 A 行中的 then()
產生拒絕,它將永遠不會在任何地方被處理。Promise 函式庫 Q 提供了一個方法 done()
,用於方法呼叫鏈中的最後一個元素。它會取代最後一個 then()
(並有一個到兩個參數)
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
done
(
f2
);
}
或者,它會插入在最後一個 then()
之後(並有零個參數)
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
then
(
f2
)
.
done
();
}
引用 Q 文件
done
與then
使用的黃金法則為:將你的承諾傳回給其他人,或者如果鏈在你這裡結束,請呼叫done
來終止它。使用catch
終止是不夠的,因為 catch 處理常式本身可能會擲回一個錯誤。
以下是你在 ECMAScript 6 中實作 done()
的方式
Promise
.
prototype
.
done
=
function
(
onFulfilled
,
onRejected
)
{
this
.
then
(
onFulfilled
,
onRejected
)
.
catch
(
function
(
reason
)
{
// Throw an exception globally
setTimeout
(()
=>
{
throw
reason
},
0
);
});
};
儘管 done
的功能顯然有用,但它尚未新增到 ECMAScript 6。這個想法是先探討引擎可以自動偵測多少。根據運作情況,可能需要引入 done()
。
finally()
有時您想要執行一個動作,而不管是否發生錯誤。例如,在您完成使用資源後進行清理。這就是 Promise 方法 finally()
的用途,它的運作方式很像例外處理中的 finally
子句。它的回呼函式不接收任何參數,但會收到已解決或已拒絕的通知。
createResource
(
···
)
.
then
(
function
(
value1
)
{
// Use resource
})
.
then
(
function
(
value2
)
{
// Use resource
})
.
finally
(
function
()
{
// Clean up
});
這是 Domenic Denicola
建議 實作 finally()
的方式
Promise
.
prototype
.
finally
=
function
(
callback
)
{
const
P
=
this
.
constructor
;
// We don’t invoke the callback in here,
// because we want then() to handle its exceptions
return
this
.
then
(
// Callback fulfills => continue with receiver’s fulfillment or rejec\
tion
// Callback rejects => pass on that rejection (then() has no 2nd para\
meter
!
)
value
=>
P
.
resolve
(
callback
()).
then
(()
=>
value
),
reason
=>
P
.
resolve
(
callback
()).
then
(()
=>
{
throw
reason
})
);
};
回呼函式決定如何處理接收器 (this
) 的解決
finally()
傳回的 Promise 的解決。在某種程度上,我們將 finally()
從方法鏈中取出。範例 1 (由 Jake Archibald 提供):使用 finally()
隱藏一個 spinner。簡化版本
showSpinner
();
fetchGalleryData
()
.
then
(
data
=>
updateGallery
(
data
))
.
catch
(
showNoDataError
)
.
finally
(
hideSpinner
);
範例 2 (由 Kris Kowal 提供):使用 finally()
移除一個測試。
const
HTTP
=
require
(
"q-io/http"
);
const
server
=
HTTP
.
Server
(
app
);
return
server
.
listen
(
0
)
.
then
(
function
()
{
// run test
})
.
finally
(
server
.
stop
);
Promise 函式庫 Q 有 工具函式,用於與 Node.js 風格的 (err, result)
回呼函式 API 介接。例如,denodeify
會將一個基於回呼函式的函式轉換成一個基於 Promise 的函式
const
readFile
=
Q
.
denodeify
(
FS
.
readFile
);
readFile
(
'foo.txt'
,
'utf-8'
)
.
then
(
function
(
text
)
{
···
});
denodify 是個微型函式庫,它只提供 Q.denodeify()
的功能,並符合 ECMAScript 6 Promise API。
市面上有很多 Promise 函式庫。以下這些函式庫符合 ECMAScript 6 API,這表示您可以立即使用它們,並在稍後輕鬆地移轉到原生 ES6。
極簡的 polyfill
較大型的 Promise 函式庫
Q.Promise
由 Kris Kowal 編寫,實作了 ES6 API。ES6 標準函式庫的 polyfill
Promise
。Promise
。透過 Promise 實作非同步函式比透過事件或回呼更方便,但仍然不是理想的作法
解決方案是將封鎖呼叫帶入 JavaScript。產生器讓我們透過函式庫來做到這一點:在以下程式碼中,我使用 控制流程函式庫 co 來非同步地擷取兩個 JSON 檔案。
co
(
function
*
()
{
try
{
const
[
croftStr
,
bondStr
]
=
yield
Promise
.
all
([
// (A)
getFile
(
'https://127.0.0.1:8000/croft.json'
),
getFile
(
'https://127.0.0.1:8000/bond.json'
),
]);
const
croftJson
=
JSON
.
parse
(
croftStr
);
const
bondJson
=
JSON
.
parse
(
bondStr
);
console
.
log
(
croftJson
);
console
.
log
(
bondJson
);
}
catch
(
e
)
{
console
.
log
(
'Failure to read: '
+
e
);
}
});
在 A 行中,執行透過 yield
封鎖(等待),直到 Promise.all()
的結果準備好。這表示程式碼在執行非同步操作時看起來像是同步的。
詳細說明請參閱 產生器章節。
在本節中,我們將從不同的角度探討 Promise:我們不會學習如何使用 API,而是會探討它的簡單實作。這個不同的角度對我理解 Promise 有很大的幫助。
Promise 實作稱為 DemoPromise
。為了更容易理解,它並未完全符合 API。但它已經足夠接近,可以讓您深入了解實際實作所面臨的挑戰。
DemoPromise
是具有三個原型方法的類別
DemoPromise.prototype.resolve(value)
DemoPromise.prototype.reject(reason)
DemoPromise.prototype.then(onFulfilled, onRejected)
也就是說,resolve
和 reject
是方法(相對於傳遞給建構函式回呼參數的函式)。
我們的首要實作是具有最小功能的獨立 Promise
then()
註冊反應(回呼)。無論 Promise 是否已解決,它都必須獨立運作。
以下是此首要實作的使用方式
const
dp
=
new
DemoPromise
();
dp
.
resolve
(
'abc'
);
dp
.
then
(
function
(
value
)
{
console
.
log
(
value
);
// abc
});
下圖說明了我們的首個 DemoPromise
如何運作
DemoPromise.prototype.then()
讓我們先檢視 then()
。它必須處理兩種情況
onFulfilled
和 onRejected
的呼叫排隊,以便在 Promise 解決時使用。onFulfilled
或 onRejected
。
then
(
onFulfilled
,
onRejected
)
{
const
self
=
this
;
const
fulfilledTask
=
function
()
{
onFulfilled
(
self
.
promiseResult
);
};
const
rejectedTask
=
function
()
{
onRejected
(
self
.
promiseResult
);
};
switch
(
this
.
promiseState
)
{
case
'pending'
:
this
.
fulfillReactions
.
push
(
fulfilledTask
);
this
.
rejectReactions
.
push
(
rejectedTask
);
break
;
case
'fulfilled'
:
addToTaskQueue
(
fulfilledTask
);
break
;
case
'rejected'
:
addToTaskQueue
(
rejectedTask
);
break
;
}
}
前一個程式碼片段使用下列輔助函式
function
addToTaskQueue
(
task
)
{
setTimeout
(
task
,
0
);
}
DemoPromise.prototype.resolve()
resolve()
的運作方式如下:如果 Promise 已解決,則它不會執行任何動作(確保 Promise 只會解決一次)。否則,Promise 的狀態會變更為 'fulfilled'
,且結果會快取在 this.promiseResult
中。接著,觸發迄今已排隊的所有履行反應。
resolve
(
value
)
{
if
(
this
.
promiseState
!==
'pending'
)
return
;
this
.
promiseState
=
'fulfilled'
;
this
.
promiseResult
=
value
;
this
.
_clearAndEnqueueReactions
(
this
.
fulfillReactions
);
return
this
;
// enable chaining
}
_clearAndEnqueueReactions
(
reactions
)
{
this
.
fulfillReactions
=
undefined
;
this
.
rejectReactions
=
undefined
;
reactions
.
map
(
addToTaskQueue
);
}
reject()
類似於 resolve()
。
我們實作的下一項功能是串連
then()
會傳回一個 Promise,而該 Promise 會使用 onFulfilled
或 onRejected
傳回的內容來解決。onFulfilled
或 onRejected
,則它們會收到的任何內容都會傳遞給 then()
傳回的 Promise。顯然地,只有 then()
會變更
then
(
onFulfilled
,
onRejected
)
{
const
returnValue
=
new
Promise
();
// (A)
const
self
=
this
;
let
fulfilledTask
;
if
(
typeof
onFulfilled
===
'function'
)
{
fulfilledTask
=
function
()
{
const
r
=
onFulfilled
(
self
.
promiseResult
);
returnValue
.
resolve
(
r
);
// (B)
};
}
else
{
fulfilledTask
=
function
()
{
returnValue
.
resolve
(
self
.
promiseResult
);
// (C)
};
}
let
rejectedTask
;
if
(
typeof
onRejected
===
'function'
)
{
rejectedTask
=
function
()
{
const
r
=
onRejected
(
self
.
promiseResult
);
returnValue
.
resolve
(
r
);
// (D)
};
}
else
{
rejectedTask
=
function
()
{
// `onRejected` has not been provided
// => we must pass on the rejection
returnValue
.
reject
(
self
.
promiseResult
);
// (E)
};
}
···
return
returnValue
;
// (F)
}
then()
會建立並傳回一個新的 Promise(第 A 和 F 行)。此外,fulfilledTask
和 rejectedTask
的設定方式不同:在解決後…
onFulfilled
的結果用於解決 returnValue
(第 B 行)。
onFulfilled
不存在,我們使用完成值來解決 returnValue
(第 C 行)。onRejected
的結果用於解決(而非拒絕!)returnValue
(第 D 行)。
onRejected
不存在,我們使用傳遞拒絕值給 returnValue
(第 E 行)。扁平化主要是讓串接更方便:通常,從反應傳回值會將它傳遞給下一個 then()
。如果我們傳回一個 Promise,最好可以為我們「解開」,就像以下範例
asyncFunc1
()
.
then
(
function
(
value1
)
{
return
asyncFunc2
();
// (A)
})
.
then
(
function
(
value2
)
{
// value2 is fulfillment value of asyncFunc2() Promise
console
.
log
(
value2
);
});
我們在第 A 行傳回一個 Promise,而且不必在目前方法內巢狀呼叫 then()
,我們可以在方法的結果上呼叫 then()
。因此:沒有巢狀 then()
,一切都保持扁平。
我們透過讓 resolve()
方法進行扁平化來實作這一點
如果我們允許 Q 成為 thenable(而不仅仅是 Promise),我們可以讓扁平化更通用。
為了實作鎖定,我們引入一個新的布林旗標 this.alreadyResolved
。一旦為 true,this
會被鎖定,而且無法再解決。請注意,this
仍可能處於待處理狀態,因為它的狀態現在與它鎖定的 Promise 相同。
resolve
(
value
)
{
if
(
this
.
alreadyResolved
)
return
;
this
.
alreadyResolved
=
true
;
this
.
_doResolve
(
value
);
return
this
;
// enable chaining
}
實際的解決現在發生在私有方法 _doResolve()
_doResolve
(
value
)
{
const
self
=
this
;
// Is `value` a thenable?
if
(
typeof
value
===
'object'
&&
value
!==
null
&&
'then'
in
value
)
{
// Forward fulfillments and rejections from `value` to `this`.
// Added as a task (versus done immediately) to preserve async semant\
ics
.
addToTaskQueue
(
function
()
{
// (A)
value
.
then
(
function
onFulfilled
(
result
)
{
self
.
_doResolve
(
result
);
},
function
onRejected
(
error
)
{
self
.
_doReject
(
error
);
});
});
}
else
{
this
.
promiseState
=
'fulfilled'
;
this
.
promiseResult
=
value
;
this
.
_clearAndEnqueueReactions
(
this
.
fulfillReactions
);
}
}
扁平化在第 A 行執行:如果 value
已完成,我們希望 self
已完成,如果 value
已拒絕,我們希望 self
已拒絕。轉發透過私有方法 _doResolve
和 _doReject
發生,以繞過 alreadyResolved
的保護。
使用串接後,Promise 的狀態會變得更複雜(如 ECMAScript 6 規範的 第 25.4 節 所述)
如果你只是使用 Promise,通常可以採用簡化的世界觀,並忽略鎖定。最重要的狀態相關概念仍然是「已結算」:如果 Promise 已完成或已拒絕,則表示已結算。Promise 結算後,它就不會再改變(狀態和完成或拒絕值)。
如果你想實作 Promise,那麼「解決」也很重要,而且現在更難理解
作為我們的最後一個特徵,我們希望我們的 Promise 能夠將使用者程式碼中的例外處理為拒絕。現在,「使用者程式碼」表示 then()
的兩個回呼參數。
以下摘錄顯示我們如何將 onFulfilled
內部的例外轉換為拒絕 – 通過在 A 行中的呼叫周圍包裝一個 try-catch
。
then
(
onFulfilled
,
onRejected
)
{
···
let
fulfilledTask
;
if
(
typeof
onFulfilled
===
'function'
)
{
fulfilledTask
=
function
()
{
try
{
const
r
=
onFulfilled
(
self
.
promiseResult
);
// (A)
returnValue
.
resolve
(
r
);
}
catch
(
e
)
{
returnValue
.
reject
(
e
);
}
};
}
else
{
fulfilledTask
=
function
()
{
returnValue
.
resolve
(
self
.
promiseResult
);
};
}
···
}
如果我們想將 DemoPromise
轉換為實際的 Promise 實作,我們仍然需要實作 揭示建構函式模式 [2]:ES6 Promise 不是透過方法解決和拒絕,而是透過傳遞給 執行器(建構函式的回呼參數)的函式。
如果執行器擲出例外,則「它的」Promise 必須被拒絕。
Promise 的一個重要優點是,非同步瀏覽器 API 將越來越多地使用它們,並統一當前多樣且不兼容的模式和慣例。讓我們來看兩個即將推出的基於 Promise 的 API。
fetch API 是 XMLHttpRequest 的基於 Promise 的替代方案
fetch
(
url
)
.
then
(
request
=>
request
.
text
())
.
then
(
str
=>
···
)
fetch()
為實際請求傳回 Promise,text()
為內容傳回 Promise,內容為字串。
用於程式化匯入模組的 ECMAScript 6 API 也基於 Promise
System
.
import
(
'some_module.js'
)
.
then
(
some_module
=>
{
···
})
與事件相比,Promise 更適合處理一次性結果。無論您是在計算結果之前還是之後註冊結果,您都將獲得結果。Promise 的這種優勢在性質上是基本的。另一方面,您不能使用它們來處理重複發生的事件。串連是 Promise 的另一個優點,但可以將其新增到事件處理中。
相較於回呼,Promise 具有更簡潔的函數(或方法)簽章。使用回呼時,參數用於輸入和輸出
fs
.
readFile
(
name
,
opts
?
,
(
err
,
string
|
Buffer
)
=>
void
)
使用 Promise 時,所有參數都用於輸入
readFilePromisified
(
name
,
opts
?
)
:
Promise
<
string
|
Buffer
>
Promise 的其他優點包括
Array.prototype.map()
。then()
和 catch()
的串接。Promise 適用於單一非同步結果。它們不適合於
ECMAScript 6 Promise 缺少兩個有時很有用的功能
Q Promise 函式庫 支援 後者,而且有 計畫 將這兩個功能新增到 Promises/A+。
本節概述 ECMAScript 6 Promise API,如 規格 中所述。
Promise
建構函式 Promise 的建構函式呼叫如下
const
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
···
});
此建構函式的回呼稱為執行器。執行器可以使用其參數來解析或拒絕新的 Promise p
resolve(x)
以 x
解析 p
x
是 thenable,其結算會轉發到 p
(包括觸發透過 then()
註冊的反應)。p
會以 x
履行。reject(e)
以值 e
拒絕 p
(通常是 Error
的執行個體)。Promise
方法 下列兩個靜態方法建立接收者的新執行個體
Promise.resolve(x)
:將任意值轉換為 Promises,並了解 Promises。
x
的建構函式是接收者,則 x
會不變地傳回。x
履行。Promise.reject(reason)
:建立一個新的接收者執行個體,並以值 reason
拒絕。直覺上,靜態方法 Promise.all()
和 Promise.race()
將 Promises 的可迭代組合成一個 Promise。也就是說
this.resolve()
轉換為 Promises。這些方法為
Promise.all(iterable)
:傳回一個 Promise,該 Promise…
iterable
中的所有元素都已履行,則會履行。Promise.race(iterable)
:iterable
中第一個已解決的元素會用於解決傳回的 Promise。Promise.prototype
方法 Promise.prototype.then(onFulfilled, onRejected)
onFulfilled
和 onRejected
稱為反應。onFulfilled
。類似地,onRejected
會得知拒絕。then()
傳回一個新的 Promise Q(透過接收者的建構函式的類型建立)
onFulfilled
,接收者的履行會轉發至 then()
的結果。onRejected
,接收者的拒絕會轉發至 then()
的結果。遺漏反應的預設值可以這樣實作
function
defaultOnFulfilled
(
x
)
{
return
x
;
}
function
defaultOnRejected
(
e
)
{
throw
e
;
}
Promise.prototype.catch(onRejected)
p.catch(onRejected)
等於 p.then(null, onRejected)
。[1] “Promises/A+”,由 Brian Cavalier 和 Domenic Denicola 編輯(JavaScript Promises 的實際標準)
[2] “The Revealing Constructor Pattern” by Domenic Denicola(此模式由 Promise
建構函式使用)