diff options
Diffstat (limited to 'files/ru/web/javascript/guide/using_promises/index.html')
-rw-r--r-- | files/ru/web/javascript/guide/using_promises/index.html | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/files/ru/web/javascript/guide/using_promises/index.html b/files/ru/web/javascript/guide/using_promises/index.html new file mode 100644 index 0000000000..d104f15d9e --- /dev/null +++ b/files/ru/web/javascript/guide/using_promises/index.html @@ -0,0 +1,322 @@ +--- +title: Использование промисов +slug: Web/JavaScript/Guide/Using_promises +tags: + - JavaScript + - Асинхронность + - Гайд + - промис +translation_of: Web/JavaScript/Guide/Using_promises +original_slug: Web/JavaScript/Guide/Ispolzovanie_promisov +--- +<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> |