aboutsummaryrefslogtreecommitdiff
path: root/files/zh-tw/web/javascript/guide/using_promises/index.html
blob: 1df6376ffd343461bdd1ab1cb74e388d6371c32d (plain)
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
---
title: 使用 Promise
slug: Web/JavaScript/Guide/Using_promises
translation_of: Web/JavaScript/Guide/Using_promises
---
<div>{{jsSidebar("JavaScript Guide")}}</div>

<p>{{jsxref("Promise")}} 是一個表示非同步運算的最終完成或失敗的物件。由於多數人使用預建立的 Promise,這個導覽會先講解回傳 Promise 的使用方式,之後再介紹如何建立。</p>

<p>基本上,一個 Promise 是一個根據附加給他的 Callback 回傳的物件,以取代傳遞 Callback 到這個函數。</p>

<p>舉例來說,下方的範例若用舊方式應該會有兩個 Callback,並根據成功或失敗來決定使用哪個:</p>

<pre class="brush: js line-numbers  language-js">function successCallback(result) {
  console.log("It succeeded with " + result);
}

function failureCallback(error) {
  console.log("It failed with " + error);
}

doSomething(successCallback, failureCallback);
</pre>

<p>而新作法會回傳一個 Promise,這樣你就可以附加 Callback:</p>

<pre class="brush: js line-numbers  language-js">let promise = doSomething();
promise.then(successCallback, failureCallback);</pre>

<p>再簡單點:</p>

<pre class="brush: js line-numbers  language-js">doSomething().then(successCallback, failureCallback);</pre>

<p>我們稱之為 <em>非同步函數呼叫</em>。這個做法有許多好處,我們接下來看看。</p>

<h2 id="保證">保證</h2>

<p>不如舊做法,一個 Promise 有這些保證:</p>

<ul>
 <li>Callback 不會在<a href="/zh-TW/docs/Web/JavaScript/EventLoop#Run-to-completion">當次的迴圈運行結束</a>前呼叫。</li>
 <li>Callback 用 .then 添加,在非同步運算結束<em></em>呼叫,像前面那樣。</li>
 <li>複 Callback 可以透過重複呼叫 .then 達成。</li>
</ul>

<p>但 Promise 主要的立即好處是串連。</p>

<h2 id="串連">串連</h2>

<p>有個常見的需求是依序呼叫兩個以上的非同步函數,我們稱之為建立 <em>Promise 鏈</em></p>

<p>看看魔術:<code>then</code> 函數回傳一個新的 Promise,不同於原本。</p>

<pre class="brush: js">let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);
</pre>

<p></p>

<pre class="brush: js">let promise2 = doSomething().then(successCallback, failureCallback);
</pre>

<p>第二個 Promise 不只代表 <code>doSomething()</code> 完成,還有<code>successCallback</code><code>failureCallback</code> ,這兩個非同步函數回傳另一個 Promise。如此一來,任何 Callback 附加給 <code>promise2</code> 會被排在 <code>successCallback</code><code>failureCallback</code> 之後。</p>

<p>基本上,每個 Promise 代表著鏈中另外一個非同步函數的完成。</p>

<p>在古時候,多個非同步函數會使用 Callback 方式,導致波動拳問題:</p>

<p><em>(原文 Pyramid of Doom 查無中文翻譯,以較常見之波動拳取代)</em></p>

<pre class="brush: js">doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);
</pre>

<p>有了新方法,我們附加 Callback 到回傳的 Promise 上,來製造<em> Promise 鏈</em></p>

<pre class="brush: js">doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
</pre>

<p><code>then</code> 的函數是選用的,以及 <code>catch(failureCallback)</code><code>then(null, failureCallback)</code> 的簡寫。你也許會想用<a href="/zh-TW/docs/Web/JavaScript/Reference/Functions/Arrow_functions">箭頭函數</a>取代:</p>

<pre class="brush: js">doSomething()
.then(result =&gt; doSomethingElse(result))
.then(newResult =&gt; doThirdThing(newResult))
.then(finalResult =&gt; {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
</pre>

<p><strong>注意:</strong>永遠要回傳結果,否則 Callback 不會獲得前一個 Promise 的結果。</p>

<h3 id="獲錯後串接">獲錯後串接</h3>

<p>失敗<em></em>的串接是可行的,也就是說 <code>catch</code> 會非常好用,即使鏈中出錯。看看這個範例:</p>

<pre class="brush: js">new Promise((resolve, reject) =&gt; {
    console.log('Initial');

    resolve();
})
.then(() =&gt; {
    throw new Error('Something failed');

    console.log('Do this');
})
.catch(() =&gt; {
    console.log('Do that');
})
.then(() =&gt; {
    console.log('Do this whatever happened before');
});
</pre>

<p>他會輸出:</p>

<pre>Initial
Do that
Do this whatever happened before
</pre>

<p>注意「Do this」沒有被輸出,因為「Something failed」錯誤導致拒絕。</p>

<h2 id="錯誤傳遞">錯誤傳遞</h2>

<p>在波動拳狀況中,你可能會看到三次 <code>failureCallback</code> ,在 Promise 鏈中只需要在尾端使用一次:</p>

<pre class="brush: js">doSomething()
.then(result =&gt; doSomethingElse(result))
.then(newResult =&gt; doThirdThing(newResult))
.then(finalResult =&gt; console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
</pre>

<p>基本上,一個 Promise 鏈遇到錯誤時會往下尋找 Catch 處理器。這是經過模組化的非同步程式:</p>

<pre class="brush: js">try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}
</pre>

<p>在 ECMAScript 2017 中,在有 <a href="/zh-TW/docs/Web/JavaScript/Reference/Statements/async_function"><code>async</code>/<code>await</code></a> 語法糖的同步程式達到高峰:</p>

<pre class="brush: js">async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}
</pre>

<p>這基於 Promise,例如 <code>doSomething()</code>和之前一樣。你可以閱讀在<a href="https://developers.google.com/web/fundamentals/getting-started/primers/async-functions">這裡</a>閱讀更多。</p>

<p>Promise 藉由捕捉所有錯誤,包含例外和程式錯誤,解決了 Callback 地獄的缺點。這是非同步運算的基本特性。</p>

<h2 id="在舊有_API_上建立_Promise">在舊有 API 上建立 Promise</h2>

<p>{{jsxref("Promise")}} 可以透過建構子建立。所以用建構子包裹舊的 API即可。</p>

<p>在理想情況,所有非同步函數都會回傳 Promise,然而許多 API 仍然用舊的方式來傳遞成功、失敗 Callback,有個典型的例子是{{domxref("WindowTimers.setTimeout", "setTimeout()")}} :</p>

<pre class="brush: js">setTimeout(() =&gt; saySomething("10 seconds passed"), 10000);
</pre>

<p>混合古代 Callback 和 Promise 是有問題的。如果 <code>saySomething</code> 失敗或有程式錯誤,那不會有任何錯誤被捕捉。</p>

<p>幸運地,我們可以用 Promise 包裹他,最好盡可能的在最底層包裹,並永遠不要再直接呼叫他們:</p>

<pre class="brush: js">const wait = ms =&gt; new Promise(resolve =&gt; setTimeout(resolve, ms));

wait(10000).then(() =&gt; saySomething("10 seconds")).catch(failureCallback);
</pre>

<p>基本上,Promise 建構子需要一個運作函數來正規地處理或拒絕 Promise。但因為 <code>setTimeout</code> 不會失敗,所以我們捨棄 reject。</p>

<h2 id="組合">組合</h2>

<p>{{jsxref("Promise.resolve()")}} 和 {{jsxref("Promise.reject()")}} 是用來正規地建立已經處理或拒絕的 Promise。他們在某些情況特別有用。</p>

<p>{{jsxref("Promise.all()")}} 和 {{jsxref("Promise.race()")}} 是兩個組合工具用於使 Promise 平行運作。</p>

<p>連續關聯是可行的,這是極簡 JavaScript 範例:</p>

<pre class="brush: js">[func1, func2].reduce((p, f) =&gt; p.then(f), Promise.resolve());
</pre>

<p>基本上,我們摺疊(Reduce)一個非同步函數陣列成一個 Promise 鏈:<code>Promise.resolve().then(func1).then(func2);</code></p>

<p>這可以用可重用的構成函數完成,通常用函數式編程:</p>

<pre class="brush: js">let applyAsync = (acc,val) =&gt; acc.then(val);
let composeAsync = (...funcs) =&gt; x =&gt; funcs.reduce(applyAsync, Promise.resolve(x));</pre>

<p><code>composeAsync</code> 接受任何數量的函數作為參數,並回傳一個接受一個初始值用來傳給組合的新函數。這個好處是無論其中函數是非同步或否,都會保證用正確的順序執行:</p>

<pre class="brush: js">let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);
</pre>

<p>ECMAScript 2017 中連續組合利用 async/await 更簡單:</p>

<pre class="brush: js">for (let f of [func1, func2]) {
  await f();
}
</pre>

<h2 id="計時">計時</h2>

<p>為了避免意外,傳給 <code>then</code> 的函數不會被同步地呼叫,即使是完成的 Promise:</p>

<pre class="brush: js">Promise.resolve().then(() =&gt; console.log(2));
console.log(1); // 1, 2
</pre>

<p>被傳入的函數會被放在子任務佇列而非立即執行,因此他會在當前的事件迴圈結束、佇列清空時執行,例如:</p>

<pre class="brush: js">const wait = ms =&gt; new Promise(resolve =&gt; setTimeout(resolve, ms));

wait().then(() =&gt; console.log(4));
Promise.resolve().then(() =&gt; console.log(2)).then(() =&gt; console.log(3));
console.log(1); // 1, 2, 3, 4
</pre>

<h2 id="看更多">看更多</h2>

<ul>
 <li>{{jsxref("Promise.then()")}}</li>
 <li><a href="http://promisesaplus.com/">Promises/A+ 特色</a></li>
 <li><a href="https://medium.com/@ramsunvtech/promises-of-promise-part-1-53f769245a53">Venkatraman.R - JS Promise (Part 1, Basics)</a></li>
 <li><a href="https://medium.com/@ramsunvtech/js-promise-part-2-q-js-when-js-and-rsvp-js-af596232525c#.dzlqh6ski">Venkatraman.R - JS Promise (Part 2 - Using Q.js, When.js and RSVP.js)</a></li>
 <li><a href="https://tech.io/playgrounds/11107/tools-for-promises-unittesting/introduction">Venkatraman.R - Tools for Promises Unit Testing</a></li>
 <li><a href="http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html">Nolan Lawson: We have a problem with promises — Common mistakes with promises</a></li>
</ul>