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
|
---
title: Использование промисов
slug: Web/JavaScript/Guide/Ispolzovanie_promisov
tags:
- JavaScript
- Асинхронность
- Гайд
- промис
translation_of: Web/JavaScript/Guide/Using_promises
---
<div>
<p>{{jsSidebar("Руководство по JavaScript")}}{{PreviousNext("Web/JavaScript/Guide/Details_of_the_Object_Model", "Web/JavaScript/Guide/Iterators_and_Generators")}}</p>
</div>
<p class="summary">{{jsxref("Promise")}} (промис, англ. "обещание") - это объект, представляющий результат успешного или неудачного завершения асинхронной операции. Так как большинство людей пользуются уже созданными промисами, это руководство начнем с объяснения использования вернувшихся промисов до объяснения принципов создания. </p>
<p>В сущности, промис - это возвращаемый объект, в который вы записываете два коллбэка вместо того, чтобы передать их функции.</p>
<p>Например, вместо старомодной функции, которая принимает два коллбэка и вызывает один из них в зависимости от успешного или неудачного завершения операции:</p>
<pre class="brush: js line-numbers language-js">function doSomethingOldStyle(successCallback, failureCallback) {
console.log("Готово.");
// Успех в половине случаев.
if (Math.random() > .5) {
successCallback("Успех")
} else {
failureCallback("Ошибка")
}
}
function successCallback(result) {
console.log("Успешно завершено с результатом " + result);
}
function failureCallback(error) {
console.log("Завершено с ошибкой " + error);
}
doSomethingOldStyle(successCallback, failureCallback);
</pre>
<p>…современные функции возвращают промис, в который вы записываете ваши коллбэки:</p>
<pre class="brush: js line-numbers language-js">function doSomething() {
return new Promise((resolve, reject) => {
console.log("Готово.");
// Успех в половине случаев.
if (Math.random() > .5) {
resolve("Успех")
} else {
reject("Ошибка")
}
})
}
const 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>В отличие от старомодных переданных коллбэков промис дает некоторые гарантии:</p>
<ul>
<li>Коллбэки никогда не будут вызваны до <a href="/ru/docs/Web/JavaScript/EventLoop#Никогда_не_блокируется">завершения обработки текущего события</a> в событийном цикле JavaScript.</li>
<li>Коллбеки, добавленные через .then даже <em>после </em>успешного или неудачного завершения асинхронной операции, будут также вызваны.</li>
<li>Несколько коллбэков может быть добавлено вызовом .then нужное количество раз, и они будут выполняться независимо в порядке добавления.</li>
</ul>
<p>Но наиболее непосредственная польза от промисов - цепочка вызовов (<em>chaining</em>).</p>
<h2 id="Цепочка_вызовов">Цепочка вызовов</h2>
<p>Общая нужда - выполнять две или более асинхронных операции одна за другой, причём каждая следующая начинается при успешном завершении предыдущей и использует результат её выполнения. Мы реализуем это, создавая цепочку вызовов промисов (<em>promise chain</em>).</p>
<p>Вот в чём магия: функция <code>then</code> возвращает новый промис, отличающийся от первоначального:</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>Второй промис представляет завершение не только <code>doSomething()</code>, но и функций <code>successCallback</code> или <code>failureCallback</code>, переданных Вами, а они тоже могут быть асинхронными функциями, возвращающими промис. В этом случае все коллбэки, добавленные к <code>promise2</code> будут поставлены в очередь за промисом, возвращаемым <code>successCallback</code> или <code>failureCallback</code>.</p>
<p>По сути, каждый вызванный промис означает успешное завершение предыдущих шагов в цепочке.</p>
<p>Раньше выполнение нескольких асинхронных операций друг за другом приводило к классической "Вавилонской башне" коллбэков:</p>
<pre class="brush: js">doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Итоговый результат: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
</pre>
<p>В современных функциях мы записываем коллбэки в возвращаемые промисы - формируем цепочку промисов:</p>
<pre class="brush: js">doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Итоговый результат: ' + finalResult);
})
.catch(failureCallback);
</pre>
<p>Аргументы <code>then</code> необязательны, а <code>catch(failureCallback)</code> - это сокращение для <code>then(null, failureCallback)</code>. Вот как это выражено с помощью <a href="/ru/docs/Web/JavaScript/Reference/Functions/Arrow_functions">стрелочных функций</a>:</p>
<pre class="brush: js">doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Итоговый результат: ${finalResult}`);
})
.catch(failureCallback);
</pre>
<p><strong>Важно:</strong> Всегда возвращайте промисы в return, иначе коллбэки не будут сцеплены и ошибки могут быть не пойманы (стрелочные функции неявно возвращают результат, если скобки {} вокруг тела функции опущены).</p>
<h3 id="Цепочка_вызовов_после_catch">Цепочка вызовов после catch</h3>
<p>Можно продолжить цепочку вызовов <em>после </em>ошибки, т. е. после <code>catch</code>, что полезно для выполнения новых действий даже после того, как действие вернет ошибку в цепочке вызовов. Ниже приведен пример:</p>
<pre class="syntaxbox"><code class="language-js"><span class="keyword token">new</span> <span class="class-name token">Promise</span><span class="punctuation token">(</span><span class="punctuation token">(</span>resolve<span class="punctuation token">,</span> reject<span class="punctuation token">)</span> <span class="operator token">=</span><span class="operator token">></span> <span class="punctuation token">{</span>
console<span class="punctuation token">.</span><span class="function token">log</span><span class="punctuation token">(</span><span class="string token">'</span></code>Начало<code class="language-js"><span class="string token">'</span><span class="punctuation token">)</span><span class="punctuation token">;</span>
</code>
resolve();
})
.then(() => {
throw new Error('Где-то произошла ошибка');
console.log('Выведи это');
})
.catch(() => {
console.log('Выведи то');
})
.then(() => {
console.log('Выведи это, несмотря ни на что');
});</pre>
<p>В результате выведется данный текст:</p>
<pre>Начало
Выведи то
Выведи это, несмотря ни на что</pre>
<p>Заметьте, что текст "Выведи это" не вывелся, потому что "Где то произошла ошибка" привела к отказу</p>
<h2 id="Распространение_ошибки">Распространение ошибки</h2>
<p>Вы могли ранее заметить, что <code>failureCallback</code> повторяется три раза в <strong>"pyramid of doom", </strong>а в цепочке промисов всего лишь один раз:</p>
<pre><code>doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`</code>Итоговый результат<code>: ${finalResult}`))
.catch(failureCallback);</code></pre>
<p>В основном, цепочка промисов останавливает выполнение кода, если где-либо произошла ошибка, и вместо этого ищет далее по цепочке обработчики ошибок. Это очень похоже на то, как работает синхронный код:</p>
<pre><code>try {
let result = syncDoSomething();
let newResult = syncDoSomethingElse(result);
let finalResult = syncDoThirdThing(newResult);
console.log(`</code>Итоговый результат<code>: ${finalResult}`);
} catch(error) {
failureCallback(error);
}</code>
</pre>
<p>Эта симметрия с синхронным кодом лучше всего показывает себя в синтаксическом сахаре <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>async</code>/<code>await</code></a> в ECMAScript 2017:</p>
<pre><code>async function foo() {
try {
let result = await doSomething();
let newResult = await doSomethingElse(result);
let finalResult = await doThirdThing(newResult);
console.log(`</code>Итоговый результат<code>: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}</code>
</pre>
<p>Работа данного кода основана на промисах. Для примера здесь используется функция <code>doSomething()</code>, которая встречалась ранее. Вы можете прочитать больше о синтаксисе <a href="https://developers.google.com/web/fundamentals/getting-started/primers/async-functions">здесь</a></p>
<p>Примисы решают основную проблему пирамид, обработку всех ошибок, даже вызовов исключений и программных ошибок. Это основа для функционального построения асинхронных операций.</p>
<h2 id="Создание_промиса_вокруг_старого_коллбэка">Создание промиса вокруг старого коллбэка</h2>
<p>{{jsxref("Promise")}} может быть создан с помощью конструктора. Это может понадобится только для старых API.</p>
<p>В идеале, все асинхронные функции уже должны возвращать промис. Но увы, некоторые APIs до сих пор ожидают успешного или неудачного коллбека переданных по старинке. Типичный пример: {{domxref("WindowTimers.setTimeout", "setTimeout()")}} функция:</p>
<pre><code>setTimeout(() => saySomething("10 seconds passed"), 10000);</code></pre>
<p>Смешивание старого коллбэк-стиля и промисов проблематично. В случае неудачного завершения <code>saySomething</code> или программной ошибки, нельзя обработать ошибку.</p>
<p>К с частью мы можем обернуть функцию в промис. Хороший тон оборачивать проблематичные функции на самом низком возможном уровне, и больше никогда их не вызывать на прямую:</p>
<pre><code>const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);</code></pre>
<p>В сущности, конструктор промиса становится исполнителем функции, который позволяет нам резолвить или режектить промис вручную. Так как <code>setTimeout</code> всегда успешен, мы опустили reject в этом случае.</p>
<h2 id="Композиция">Композиция</h2>
<p>{{jsxref("Promise.resolve()")}} и {{jsxref("Promise.reject()")}} короткий способ создать уже успешные или отклоненные промисы соответственно. Это иногда бывает полезно.</p>
<p>{{jsxref("Promise.all()")}} и {{jsxref("Promise.race()")}} - два метода запустить асинхронные операции параллельно.</p>
<p>Последовательное выполнение композиции возможно при помощи хитрости JavaScript:</p>
<pre><code>[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());</code></pre>
<p>Фактически, мы превращаем массив асинхронных функций в цепочку промисов равносильно: <code>Promise.resolve().then(func1).then(func2);</code></p>
<p>Это также можно сделать, объеденив композицию в функцию, в функциональном стиле программирования:</p>
<pre><code>const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));</code></pre>
<p><code>composeAsync</code> функция примет любое количество функций в качестве аргументов и вернет новую функцию которая примет в параметрах начальное значение, переданное по цепочке. Это удобно, потому что некоторые или все функции могут быть либо асинхронными либо синхронными, и они гарантированно выполнятся в правильной последовательности:</p>
<pre><code>const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);</code></pre>
<p>В ECMAScript 2017, последовательные композиции могут быть выполненны более простым способом с помощью async/await:</p>
<pre><code>for (const f of [func1, func2]) {
await f();
}</code></pre>
<h2 id="Порядок_выполнения">Порядок выполнения</h2>
<p>Чтобы избежать сюрпризов, функции, переданные в <code>then</code> никогда не будут вызванны синхронно, даже с уже разрешенным промисом:</p>
<pre><code>Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2</code></pre>
<p>Вместо немедленного выполнения, переданная функция встанет в очередь микрозадач, а значит выполнится, когда очередь будет пустой в конце текущего вызова JavaScript цикла событий (event loop), т.е. очень скоро:</p>
<pre><code>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</code></pre>
<h2 id="Вложенность">Вложенность</h2>
<p>Простые цепочки promise лучше оставлять без вложений, так как вложеность может быть результатом небрежной структуры. Смотрите <a href="https://developer.mozilla.org/ru/docs/Web/JavaScript/Guide/Ispolzovanie_promisov$edit#Common_mistakes">распространенные ошибки</a>.</p>
<p>Вложенность - это управляющая структура, ограничивающая область действия операторов catch. В частности, вложенный catch только перехватывает сбои в своей области и ниже, а не ошибки выше в цепочке за пределами вложенной области. При правильном использовании это дает большую точность в извлечение ошибок:</p>
<pre><code>doSomethingCritical()
.then(result => doSomethingOptional()
.then(optionalResult => doSomethingExtraNice(optionalResult))
.catch(e => {})) // Игнорируется если необязательные параметр не выкинул исключение
.then(() => moreCriticalStuff())
.catch(e => console.log("Критическая ошибка: " + e.message));</code></pre>
<p>Обратите внимание, что необязательный шаги здесь выделены отступом.</p>
<p>Внутренний оператор catch нейтрализует и перехватывает ошибки только от doSomethingOptional() и doSomethingExtraNice(), после чего код возобновляется с помощью moreCriticalStuff(). Важно, что в случае сбоя doSomethingCritical() его ошибка перехватывается только последним (внешним) catch.</p>
<h2 id="Частые_ошибки">Частые ошибки</h2>
<p>В этом разделе собраны частые ошибки, возникающие при создании цепочек обещаний. Несколько таких ошибок можно увидеть в следующем примере:</p>
<pre><code>// Плохой пример! Три ошибки!
doSomething().then(function(result) {
doSomethingElse(result) // Забыл вернуть обещание из внутренней цепочки + неуместное влаживание
.then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Забыл закончить цепочку методом catch</code></pre>
<p>Первая ошибка это неправильно сцепить вещи между собой. Такое происходит когда мы создаем промис но забываем вернуть его. Как следствие, цепочка сломана, но правильнее было бы сказать что теперь у нас есть две независимые цепочки, соревнующиеся за право разрешится первой. Это означает <code>doFourthThing()</code> не будет ждать <code>doSomethingElse()</code> или <code>doThirdThing()</code> пока тот закончится, и будет исполнятся параллельно с ними, это ,вероятно, не то что хотел разработчик. Отдельные цепочки также имеют отдельную обработку ошибок, что приводит к необработанным ошибкам.</p>
<p>Вторая ошибка это излишняя вложенность, включая первую ошибку. Вложенность также ограничивает область видимости внутренних обработчиков ошибок, если это не то чего хотел разработчик, это может привести к необработанным ошибкам. Примером этого является <a href="https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it">пример как не нужно создавать обещания</a>, который комбинирует вложенность с чрезмерным использованием конструктора обещаний для оборачивания кода который уже использует промисы.</p>
<p>Третяя ошибка это забыть закончить цепочку ключевым словом <code>catch</code>. Незаконченные цепочки приводят к необработанным отторжениям обещаний в большинстве браузеров.</p>
<p>Хорошим примером является всегда либо возвращать либо заканчивать цепочки обещаний, и как только вы получаете новое обещание, возвращайте его сразу же, чтобы не усложнять код излишней вложенностью:</p>
<pre><code>doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));</code></pre>
<p>Обратите внимание что <code>() => x</code> это сокращенная форма <code>() => { return x; }</code>.</p>
<p>Теперь у нас имеется единственная определенная цепочка с правильной обработкой ошибок.</p>
<p>Использование <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>async</code>/<code>await</code></a> предотвращает большинство, если не все вышеуказанные ошибки—но взамен появляется другая частая ошибка—забыть ключевое слово <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"><code>await</code></a>.</p>
<h2 id="Смотрите_также">Смотрите также</h2>
<ul>
<li>{{jsxref("Promise.then()")}}</li>
<li><a href="http://promisesaplus.com/">Спецификация Promises/A+ (EN)</a></li>
<li><a href="http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html">Нолан Лоусон (Nolan Lawson): У нас проблемы с промисами - распространенные ошибки (EN)</a></li>
</ul>
|