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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
|
---
title: Sử dụng Promise
slug: Web/JavaScript/Guide/Using_promises
tags:
- Hướng dẫn
- JavaScript
- Promise
- bất đồng bộ
- trình độ trung cấp
translation_of: Web/JavaScript/Guide/Using_promises
---
<div>{{jsSidebar("JavaScript Guide")}}{{PreviousNext("Web/JavaScript/Guide/Details_of_the_Object_Model", "Web/JavaScript/Guide/Iterators_and_Generators")}}</div>
<p class="summary">{{jsxref("Promise")}} là một đối tượng thể hiện cho sự hoàn thành hoặc thất bại của một tiến trình bất đồng bộ. Vì đa số chúng ta là người sử dụng Promise được tạo sẵn, bài viết này sẽ hướng dẫn cách sử dụng Promise trước khi hướng dẫn cách tạo ra chúng.</p>
<p>Về cơ bản, một promise là một đối tượng trả về mà bạn gắn callback vào nó thay vì truyền callback vào trong một hàm.</p>
<p>Giả sử chúng ta có một hàm, <code>createAudioFileAsync()</code>, mà nó sẽ tạo ra một file âm thanh từ config object và hai hàm callback một cách bất đồng bộ, một hàm sẽ được gọi khi file âm thanh được tạo thành công, và một hàm được gọi khi có lỗi xảy ra.</p>
<p>Sau đây là code ví dụ sử dụng <code>createAudioFileAsync()</code>:</p>
<pre class="brush: js line-numbers language-js">function successCallback(result) {
console.log("Audio file ready at URL: " + result);
}
function failureCallback(error) {
console.log("Error generating audio file: " + error);
}
createAudioFileAsync(audioSettings, successCallback, failureCallback);
</pre>
<p>Thay vì như trên, các hàm bất đồng bộ hiện đại sẽ trả về đối tượng promise mà bạn sẽ gắn callback vào nó:</p>
<p>Nếu hàm <code>createAudioFileAsync()</code> được viết lại sử dụng promise, thì việc sử dụng nó sẽ chỉ đơn giản như sau:</p>
<pre class="brush: js">createAudioFileAsync(audioSettings).then(successCallback, failureCallback);</pre>
<p>Nếu viết dài dòng hơn thì sẽ là:</p>
<pre class="brush: js line-numbers language-js">const promise = createAudioFileAsync(audioSettings);
promise.then(successCallback, failureCallback);</pre>
<p>Chúng ta gọi đây là <em>một lời gọi hàm bất đồng bộ</em> (<em>asynchronous function call)</em>. Cách tiếp cận này có nhiều ưu điểm, mà chúng ta sẽ lần lượt xem xét bên dưới.</p>
<h2 id="Sự_đảm_bảo">Sự đảm bảo</h2>
<p>Không như cách truyền callback kiểu cũ, một promise có những đảm bảo như sau:</p>
<ul>
<li>Callback sẽ không bao giờ được gọi trước khi <a href="/en-US/docs/Web/JavaScript/EventLoop#Run-to-completion">hoàn tất lượt chạy</a> của một <em>JavaScript event loop</em>.</li>
<li>Callback được thêm vào <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then">then()</a></code> <em>sau khi</em> tiến trình bất đồng bộ đã hoàn thành vẫn được gọi, và theo nguyên tắc ở trên.</li>
<li>Nhiều callback có thể được thêm vào bằng cách gọi <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then">then()</a></code> nhiều lần. Mỗi callback sẽ được gọi lần lượt, theo thứ tự mà chúng được thêm vào.</li>
</ul>
<p>Một trong những đặc tính tuyệt vời của promise là <strong>chaining</strong> (gọi nối).</p>
<h2 id="Chaining_(gọi_nối)">Chaining (gọi nối)</h2>
<p>Có một nhu cầu phổ biến đó là thực thi hai hay nhiều tiến trình bất đồng độ liên tiến nhau, cái sau bắt đầu ngay khi cái trước hoàn tất, với giá trị truyền vào là kết quả từ bước trước đó. Mục tiêu này có thể đạt được với một <em>chuỗi promise</em> (<strong>promise chain</strong>).</p>
<p>Sau đây là cách nó hoạt động: hàm <code>then()</code> trả về một <strong>promise mới</strong>, khác với hàm ban đầu:</p>
<pre class="brush: js">const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
</pre>
<p>hoặc</p>
<pre class="brush: js">const promise2 = doSomething().then(successCallback, failureCallback);
</pre>
<p>Promise thứ hai (<code>promise2</code>) không chỉ đại diện cho việc hoàn thành <code>doSomething()</code> mà còn cho cả <code>successCallback</code> hoặc <code>failureCallback</code> mà bạn đưa vào, mà chúng có thể là những hàm bất đồng bộ khác trả về promise. Trong trường hợp đó, bất kỳ callback nào được thêm vào cho <code>promise2</code> cũng sẽ được xếp phía sau promise trả về bởi một trong hai <code>successCallback</code> hoặc <code>failureCallback</code>.</p>
<p>Tóm lại, mỗi promise đại diện cho việc hoàn tất của một bước bất đồng bộ trong chuỗi.</p>
<p>Trước khi có promise, kết quả của việc thực hiện một chuỗi các thao tác bất đồng bộ theo cách cũ là một "thảm họa" kim tự tháp callback:</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>Thay vào đó, với cách tiếp cận hiện đại, chúng ta sẽ gắn các callback vào các promise trả về, tạo thành một chuỗi promise:</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>Tham số cho <code>then</code> là không bắt buộc, và <code>catch(failureCallback)</code> là cách viết gọn của <code>then(null, failureCallback)</code>. Bạn có thể thấy chuỗi promise dùng với <a href="/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions">arrow function</a> như sau:</p>
<pre class="brush: js">doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
</pre>
<p><strong>Quan trọng:</strong> hãy nhớ luôn trả về kết quả, nếu không, callback sẽ không nhận được kết quả từ promise trước đó (với arrow functions <code>() => x</code> được rút gọn từ <code>() => { return x; }</code>)</p>
<h3 id="Gọi_nối_sau_hàm_catch">Gọi nối sau hàm catch</h3>
<p>Bạn có thể tiếp tục gọi chuỗi <code>then</code> sau một hàm bắt lỗi <code>catch</code>. Điều này cho phép code của bạn luôn thực hiện một thao tác nào đó cho dù đã có lỗi xảy ra ở một bước nào đó trong chuỗi. Hãy xem ví dụ sau:</p>
<pre class="brush: js">new Promise((resolve, reject) => {
console.log('Initial');
resolve();
})
.then(() => {
throw new Error('Something failed');
console.log('Do this');
})
.catch(() => {
console.log('Do that');
})
.then(() => {
console.log('Do this, no matter what happened before');
});
</pre>
<p>Đoạn code này sẽ log ra những dòng sau:</p>
<pre>Initial
Do that
Do this, no matter what happened before
</pre>
<p><strong>Ghi chú:</strong> Dòng text <q>Do this</q> không hiển thị bởi vì Error <q>Something failed</q> đã xảy ra trước và gây lỗi trong chuỗi promise.</p>
<h2 id="Xử_lý_lỗi_tập_trung">Xử lý lỗi tập trung</h2>
<p>Bạn hãy nhớ lại đoạn code kim tự tháp thảm họa ở trên, có đến 3 lần hàm <code>failureCallback</code> được sử dụng. Trong khi đó, bạn chỉ cần khai báo một lần vào cuối chuỗi promise:</p>
<pre class="brush: js">doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
</pre>
<p>Về căn bản, một chuỗi promise sẽ dừng lại nếu có lỗi xảy ra, và nó sẽ truy xuống dưới cuối chuỗi để tìm và gọi hàm xử lý lỗi <code>catch</code>. Cách hoạt động này khá giống với cách thức hoạt động của <code>try catch</code> của code đồng bộ:</p>
<pre class="brush: js">try {
const result = syncDoSomething();
const newResult = syncDoSomethingElse(result);
const finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
</pre>
<p>Và vì lý do trên, <code>try catch</code> cũng được sử dụng để bắt lỗi cho code bất đồng bộ khi viết với cú pháp <a href="/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>async</code>/<code>await</code></a> của ECMAScript 2017.</p>
<pre class="brush: js">async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}
</pre>
<p>Cú pháp trên được xây dựng từ Promise, VD: <code>doSomething()</code> chính là hàm được viết với Promise ở trên. Bạn có thể đọc thêm về cú pháp đó <a href="https://developers.google.com/web/fundamentals/getting-started/primers/async-functions">ở đây</a>.</p>
<p>Promise giúp giải quyết một hạn chế cơ bản của kim tự tháp callback, đó là cho phép bắt được tất cả các loại lỗi, từ những lỗi <em>throw Error</em> cho đến lỗi về cú pháp lập trình. Điều này rất cần thiết cho việc phối hợp các hàm xử lý bất đồng bộ.</p>
<h2 id="Tạo_Promise_từ_API_có_kiểu_callback_cũ">Tạo Promise từ API có kiểu callback cũ</h2>
<p>Một {{jsxref("Promise")}} có thể được tạo mới từ đầu bằng cách sử dụng hàm constructor. Tuy nhiên cách này thường chỉ dùng để bọc lại API kiểu cũ.</p>
<p>Trong môi trường lý tưởng, tất cả các hàm bất đồng bộ đều trả về promise. Tuy nhiên vẫn còn nhiều API yêu cầu hàm callback được truyền vào theo kiểu cũ. Ví dụ điển hình nhất chính là hàm {{domxref("WindowTimers.setTimeout", "setTimeout()")}}:</p>
<pre class="brush: js">setTimeout(() => saySomething("10 seconds passed"), 10000);
</pre>
<p>Trộn lẫn callback và promise có nhiều vấn đề tiềm ẩn. Nếu hàm <code>saySomething()</code> xảy ra lỗi bên trong nó, sẽ không có gì bắt được lỗi này. <code>setTimeout</code> là để đổ lỗi cho điều này.</p>
<p>May mắn là chúng ta có thể bọc <code>setTimeout</code> lại với promise. Cách làm tốt nhất là bọc hàm có vấn đề ở cấp thấp nhất, để rồi sau đó chúng ta không phải gọi nó trực tiếp nữa:</p>
<pre class="brush: js">const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);
</pre>
<p>Về căn bản, constructor của Promise nhận vào một hàm thực thi với hai tham số hàm resolve và reject để chúng ta có thể giải quyết hoặc từ chối promise một cách thủ công. Bởi vì hàm <code>setTimeout()</code> không bao giờ gây ra lỗi, chúng ta bỏ qua reject trong trường hợp này.</p>
<h2 id="Phối_hợp_các_Promise">Phối hợp các Promise</h2>
<p>{{jsxref("Promise.resolve()")}} và {{jsxref("Promise.reject()")}} là những phương thức để lấy nhanh một promise đã được giải quyết hoặc từ chối sẵn. Những phương thức này có thể hữu dụng trong một số trường hợp.</p>
<p>{{jsxref("Promise.all()")}} và {{jsxref("Promise.race()")}} là hai hàm tiện ích dùng để phối hợp các thao tác bất đồng bộ chạy song song.</p>
<p>Chúng ta có thể cho các tiến trình bất đồng bộ bắt đầu song song và chờ cho đến khi tất cả đều hoàn tất như sau:</p>
<pre class="brush: js">Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });
</pre>
<p>Việc phối hợp các tiến trình bất đồng bộ diễn ra tuần tự không có sẵn tiện ích nhưng có thể viết mẹo với <code>reduce</code> như sau:</p>
<pre class="brush: js">[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });
</pre>
<p>Về cơ bản, chúng ta đang rút gọn (<em>reduce</em>, tạm dịch) một mảng các hàm bất đồng bộ thành một chuỗi promise: <code>Promise.resolve().then(func1).then(func2).then(func3);</code></p>
<p>Thao tác trên có thể được viết thành một hàm dùng lại được, như cách chúng ta hay làm trong <em>functional programming</em>:</p>
<pre class="brush: js">const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));</pre>
<p>Hàm <code>composeAsync()</code> sẽ nhận vào tham số là các hàm xử lý bất đồng bộ với số lượng bất kỳ, và trả và một hàm mới mà khi gọi, nó nhận vào một giá trị ban đầu mà giá trị này sẽ được truyền vào tuần tự qua các hàm xử lý bất đồng bộ:</p>
<pre class="brush: js">const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
</pre>
<p>Trong ECMAScript 2017, việc phối hợp tuần tự các promise có thể thực hiện đơn giản hơn với async/await:</p>
<pre class="brush: js">let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result (i.e. result3) */
</pre>
<h2 id="Thời_điểm_thực_thi">Thời điểm thực thi</h2>
<p>Để nhất quán và tránh những bất ngờ, các hàm truyền vào cho <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then">then()</a></code> sẽ không bao giờ được thực thi đồng bộ, ngay với cả những promíe đã được giải quyết:</p>
<pre class="brush: js">Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
</pre>
<p>Thay vì chạy ngay lập tức, promise callback được đặt vào hàng đợi <strong>microtask</strong>, điều này có nghĩa là nó sẽ chỉ được thực thi khi hàng đợi được làm rỗng ( các promise đều được giải quy) cuối event loop hiện tại của JavaScript:</p>
<pre class="brush: js">const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4
</pre>
<h2 id="Lồng_cấp">Lồng cấp</h2>
<p>Chuỗi Promise đơn giản chỉ nên có một cấp và không lồng vào nhau, vì lồng cấp sẽ dẫn đến những tổ hợp phức tạp dễ gây ra lỗi. Xem <a href="#Một_số_sai_lầm_phổ_biến">các sai lầm thường gặp</a>.</p>
<p>Lồng nhau là một cấu trúc kiểm soát để giới hạn phạm vi của các câu lệnh <code>catch</code>. Cụ thể, một câu lệnh <code>catch</code> lồng bên trong chỉ có thể bắt được những lỗi trong phạm vi của nó và bên dưới, không phải là lỗi phía bên trên của chuỗi bên ngoài phạm vi lồng nhau. Khi được sử dụng một cách hợp lý, nó mang lại độ chính xác cao hơn trong việc khôi phục lỗi:</p>
<pre class="brush: js">doSomethingCritical()
.then(result => doSomethingOptional()
.then(optionalResult => doSomethingExtraNice(optionalResult))
.catch(e => {})) // Ignore if optional stuff fails; proceed.
.then(() => moreCriticalStuff())
.catch(e => console.log("Critical failure: " + e.message));
</pre>
<p>Lưu ý rằng các bước tuỳ chọn ở đây được lồng vào trong, không phải từ việc thụt đầu dòng, mà từ vị trí đặt dấu <code>(</code> và <code>)</code> xung quanh chúng.</p>
<p>Câu lệnh <code>catch</code> bên trong chỉ bắt lỗi từ <code>doSomethingOptional()</code> và <code>doSomethingExtraNice()</code>, sau đó nó sẽ tiếp tục với <code>moreCriticalStuff()</code>. Điều quan trọng là nếu <code>doSomethingCritical()</code> thất bại, lỗi của nó chỉ bị bắt bởi <code>catch</code> cuối cùng (bên ngoài).</p>
<h2 id="Một_số_sai_lầm_phổ_biến">Một số sai lầm phổ biến</h2>
<p>Dưới đây là một số lỗi phổ biến cần chú ý khi sử dụng chuỗi promise. Một số trong những sai lầm này biểu hiện trong ví dụ sau:</p>
<pre class="brush: js example-bad">// Một ví dụ có đến 3 sai lầm!
doSomething().then(function(result) {
doSomethingElse(result) // Quên trả về promise từ chuỗi lồng bên trong + lồng nhau không cần thiết
.then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Quên kết thúc chuỗi bằng một hàm catch!
</pre>
<p>Sai lầm đầu tiên là không kết nối mọi thứ với nhau đúng cách. Điều này xảy ra khi chúng ta tạo ra một promise mới nhưng quên trả lại. Hậu quả là chuỗi bị hỏng, hay đúng hơn, chúng ta có hai chuỗi độc lập cùng chạy. Có nghĩa là <code>doFourthThing()</code> sẽ không đợi <code>doSomethingElse()</code> hoặc <code>doThirdThing()</code> kết thúc và sẽ chạy song song với chúng, có khả năng ngoài ý muốn. Các chuỗi riêng biệt cũng sẽ xử lý lỗi riêng biệt, dẫn đến khả năng lỗi không được xử lý.</p>
<p>Sai lầm thứ hai là làm lồng nhau không cần thiết, cho phép sai lầm đầu tiên. Lồng nhau cũng giới hạn phạm vi của các trình xử lý lỗi bên trong, điều mà nếu không cố ý thì có thể dẫn đến các lỗi chưa được xử lý. Một biến thể của điều này là <a href="https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it">promise constructor anti-pattern</a>, kết hợp lồng với việc sử dụng dự phòng của promise constructor để bọc mã đã sử dụng promise.</p>
<p>Sai lầm thứ ba là quên chấm dứt chuỗi với <code>catch</code>. Chuỗi promise không kết thúc bằng <code>catch</code>, khi bị reject sẽ gây ra lỗi "uncaught promise rejection" trong hầu hết các trình duyệt.</p>
<p>Một nguyên tắc tốt là luôn luôn trả lại hoặc chấm dứt chuỗi promise, và ngay khi bạn nhận được một promise mới, hãy trả lại ngay lập tức:</p>
<pre class="brush: js example-good">doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));
</pre>
<p>Lưu ý rằng <code>() => x</code> được rút gọn từ <code>() => { return x; }</code>.</p>
<p>Bây giờ chúng ta có một chuỗi xác định duy nhất với xử lý lỗi thích hợp.</p>
<p>Sử dụng <a href="/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>async</code>/<code>await</code></a> sẽ khắc phục hầu hết, nếu không muốn nói là tất cả các vấn đề trên đây — với một hạn chế cũng là một lỗi thường gặp với cú pháp này đó là việc quên từ khoá <a href="/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>await</code></a>.</p>
<h2 id="Xem_thêm">Xem thêm</h2>
<ul>
<li>{{jsxref("Promise.then()")}}</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>async</code>/<code>await</code></a> </li>
<li><a href="http://promisesaplus.com/">Promises/A+ specification</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>
<p>{{PreviousNext("Web/JavaScript/Guide/Details_of_the_Object_Model", "Web/JavaScript/Guide/Iterators_and_Generators")}}</p>
|