(Руководство для начинающих)

Введение

Я думаю, что большинство хороших историй начинаются с трюизма, так что давайте сделаем это. Лучшие программисты ленивы. Им настолько нравится быть ленивым, что они не спят всю ночь, изобретая новый, более эффективный способ лениться, и вступают в яростные споры с двадцатью тысячами своих лучших друзей о том, какая форма лени на самом деле является самой ленивой (и, следовательно, самый лучший).

А если серьезно, то поиск способов просто делать меньше является ключевой частью создания удобочитаемых, удобных в обслуживании и эффективных приложений. Вы обнаружите, что в кодировании очень мало того, что мы делаем, делается «ради него самого» - каждый инструмент, который мы используем (надеюсь), упрощает выполнение того, что нам нужно.

Обещания ничем не отличаются. Когда вы думаете о них абстрактно, может быть сложно понять, что именно они из себя представляют и как их использовать. Понимать их концептуально в первую очередь, на мой взгляд, игра для болванов. Очень органично, когда вы создаете какое-нибудь приложение, наступит момент, когда вы посмотрите на свой код и скажете: «Вот дерьмо. Что мне действительно нужно здесь сделать, так это убедиться, что моя программа не пытается использовать то, что я послал, чтобы получить, пока это действительно не исчезнет и не получит это ». И я с радостью готов поспорить с вами на доллар в Интернете, что именно в этот момент обещания неожиданно для вас сработают.

Давай посмотрим, смогу ли я помочь тебе туда добраться. Чтобы поговорить о том, что делают обещания, нам нужна небольшая информация о том, зачем они нам нужны. Давайте поговорим о том, что происходит, когда вы говорите узлу или V8 запустить файл. Это поможет нам понять, в чем заключаются проблемы управления асинхронными действиями, как они возникают и почему обещания являются таким замечательным и удобным решением этих проблем. Делая это, мы, вероятно, пойдем по довольно знакомой земле. Однако цель этой статьи - не просто дать вам некоторую неформальную документацию с обещаниями, но и уместить их в повествование о том, как мы создаем наши приложения. Так что терпите меня, если начало кажется немного… простым.

Часть I. Земля до обещаний

Как обычно выполняется наш код? Каждое выражение выполняется, а затем переходит к следующему. Он однонаправленный, правда? Это здорово, потому что это предсказуемо. Когда мы вызываем функцию в строке пятьдесят, мы ожидаем, что у нее будет доступ к любой информации, которая была установлена ​​в строке сорок. Без сюрпризов. И если все, что мы делаем, это запускаем код, который содержится в одном пространстве и выполняется с нормальной скоростью, именно так он и будет работать.

Но иногда нам приходится делать такие большие или сложные вещи, что даже наши сверхбыстрые компьютеры делают их медленно - по вычислительным стандартам - вещи, которые могут занять сотни миллисекунд. Как разговор с базой данных или чтение файла.

JavaScript - это однопоточный язык, а это значит, что он может делать только одну вещь за раз. Это предсказуемо и легко управляемо. Но если это буквально правда - что мы никогда не сможем перейти ко второму, пока первое не завершится, - тогда у нас не может быть всего гладкого интерактивного опыта с нашими приложениями, которые в 2018 году кажутся полностью банальна и на которую мы абсолютно полагаемся. Если бы нам пришлось просто подождать целую секунду или две, которые могут потребоваться для выборки фрагмента данных, прежде чем изменять что-либо еще о странице или приложении, у нас не могло бы быть даже очень простого поведения веб-страницы, например, наличия цвета текста изменение в ответ на наведение указателя мыши, в то время как что-либо еще происходит. Изучить JavaScript просто и легко из-за его гибкости.

К счастью, все не так просто. Стек вызовов - не единственное, что управляет нашим кодом. Наряду с этим у нас есть ряд API-интерфейсов (веб-интерфейсов или C ++, в зависимости от того, на чем мы работаем: передний или задний конец), которые помогают нам обрабатывать асинхронные действия.

Итак, если мы сделаем это:

Этот fs.ReadFile запустится и сразу же выйдет из нашего стека вызовов, освобождая нас для выполнения оператора console.log в строке двадцать. Отлично, правда? За исключением одной довольно серьезной проблемы: в console.log не будет нашего файла veryImportantFile - строка двадцать будет выполняться задолго до того, как fs.ReadFile когда-либо вернется с данными, чтобы заполнить наш материал. И наш код по-прежнему выполняется однонаправленно: мы не можем просто выполнить резервное копирование DC Al Coda к объявлению переменной всякий раз, когда это удобно для нас. Так что же нам делать?

Единственное, что мы можем сделать, - это дать этой асинхронной функции (fs.ReadFile) функцию обратного вызова и поместить туда все, что нам нужно сделать с данными. Когда асинхронная функция принимает обратный вызов, этот аргумент запускается после завершения асинхронного действия, верно? Не совсем.

Мы не знаем, сколько времени потребуется для выполнения того или иного асинхронного действия. Может быть десятая часть секунды, может быть две секунды, кто знает? Если бы мы могли каким-то образом запускать обратный вызов async всякий раз, когда он завершился, теоретически он мог бы просто выполняться в середине какой-то другой функции, что было бы странно и, вероятно, что-то сломало. Это полностью лишит нас предсказуемости, на которую мы рассчитываем. К счастью, это не так.

Когда асинхронное действие выполняет свою задачу, функция обратного вызова помещается в очередь задач, но не запускается там. Единственное место, где будет выполняться наш обратный вызов, - это стек вызовов, как и любая другая функция. А то, как оно попадает из очереди в стек, обрабатывается циклом событий.

У цикла обработки событий всего одна задача: он постоянно наблюдает за стеком вызовов. Когда стек вызовов пуст, цикл обработки событий проверяет очередь задач и захватывает первую операцию, ожидающую в очереди, для помещения в стек. На практике это означает, что наша функция обратного вызова будет запускаться в первой точке после завершения асинхронного действия, когда стек вызовов очищается. Что не совсем предсказуемо, но звучит намного лучше, чем то, что мы были минуту назад, верно?

Итак, на этом этапе наш код будет выглядеть примерно так:

На это не так уж и страшно смотреть, правда? На данный момент это не хуже, чем использование любого старого метода массива - array.map () - это не то, что мы думаем как действительно запутывание нашего кода. Но что, если вместо того, чтобы просто регистрировать эту информацию, нам действительно нужно сделать с ней что-то более существенное? Что, если, скажем, мы полагаемся на некоторые данные в veryImportantFile.txt, чтобы направить нас к следующему файлу, который нам нужно прочитать? Это было бы еще одно асинхронное действие, не так ли? И у этого был бы собственный обратный вызов, который запускался бы после того, как оба процесса завершились и стек вызовов очистился как минимум дважды. А что, если этот файл был чем-то, с чем нам действительно нужно было сделать что-то асинхронно ...

Да, и пока я не забыл: я уже упоминал, что мы на самом деле пропускаем шаг здесь. Что, если, когда наша программа отправится на получение нашего файла veryImportantFile, что-то пойдет не так? Может быть, в названии допущена опечатка, может быть, файл поврежден, может быть, возникла какая-то другая проблема, которую мы не могли предвидеть. Что тогда? Чтобы наша функция обратного вызова имела какую-либо цель, она, по-видимому, полагается на те результаты, которых мы ждали, поэтому не имеет смысла запускать ту же функцию, если мы возвращаем кучу сообщений об ошибках.

Ванильные асинхронные обратные вызовы фактически принимают в качестве аргументов две функции - одну для обработки успешного случая, а другую - для ошибок. И они на самом деле «сначала ошибка» (например, первый параметр функции будет обрабатываться как обратный вызов случая ошибки) - просто для того, чтобы сделать вещи еще менее интуитивно понятными и легкими для анализа людьми. Нашему приложению совсем не нужно сильно усложнять, прежде чем наш код начнет выглядеть следующим образом:

Вы можете сказать: «Ну, это заноза в заднице, но это работает, не так ли? Почему бы просто не засосать и не написать код? Нашей среде выполнения все равно, красив наш код или уродлив, не так ли? Зачем нам? »

Нам следует заботиться. Такая настройка означает, что наш код не переносится - когда мы запрашиваем данные, мы должны управлять абсолютно всем, что мы хотим с ними делать, с помощью одной функции обратного вызова. Нелегко создать цепочку задач, которые взаимозависимы друг от друга, и по-прежнему нелегко управлять порядком операций.

Все это грубое, грубое вложение означает, что наш код будет очень сложно отлаживать. Что могло бы быть не концом света, если бы вы просто затаили дыхание, один раз тщательно закодировали свое приложение и перешли к следующему, чтобы никогда не вернуться. Но в реальности программирования так никогда не бывает. Мне больно говорить вам об этом, но в какой-то момент вы, вероятно, сделаете ошибку в своем коде (хотя, когда вы это сделаете, вы всегда можете проверить мое базовое руководство по отладке), и я не завидую беднякам. sap, который должен продираться через этот бесконечный клубок ада обратных вызовов, пытаясь найти одинокую висящую скобку или неправильно назначенную переменную. И нет никакой гарантии, что эту отладку будет выполнять человек, который в первую очередь написал код - очень немногие задачи разработчика в реальном мире решаются от начала до конца с помощью одиноких JavaScript Cowboys. Таким образом, у вас может даже не остаться смутного воспоминания о том, что вы изначально установили код, когда вы начнете эту массовую охоту за ошибками. Не говоря уже о том, что рано или поздно вы или кто-то другой захотите провести рефакторинг этого кода, чтобы освободить место для новых или обновленных функций.

Это уродство не только косметическое. Это оказывает реальное негативное влияние на то, насколько хорошо работает ваш код, и насколько хорошо вы и ваша команда сможете собрать и поддерживать его. Если бы это было все, что у нас было, мы бы обошлись, но ... что, если бы был способ получше?

Часть II: Земля не раньше обещаний

Спойлер: есть. Итак, что такое обещание и как оно поможет нам выбраться из беспорядка, который мы, по всей видимости, создавали все это время?

Спецификация A + определяет обещание следующим образом: Промис представляет собой конечный результат асинхронной операции. Основной способ взаимодействия с обещанием - через его метод then, который регистрирует обратные вызовы для получения либо конечного значения обещания, либо причины, по которой обещание не может быть выполнено.

Это имело для вас какое-то значение? Не волнуйтесь, как я уже сказал, это игра болвана. Вместо этого давайте поговорим о том, что делает обещание. Обещание - это в основном контейнер для асинхронного действия - это говорит нам о том, что, как минимум, обещания должны давать нам все с точки зрения возможностей, которые дают нам обычные асинхронные обратные вызовы, потому что обещание - это то, что принимает ванильный асинхронный обратный вызов и поместив его в формат, который будет менее отвратительным для реализации. В противном случае в них не было бы никакого смысла - помните, как мы раньше говорили о том, что все это делается во имя облегчения нашей работы?

Обещания полностью освободят нас от вложенности. Обещания дадут нам действительно простой путь для отслеживания взаимосвязанных действий. Обещания дадут нам несколько вариантов одновременной или настраиваемой обработки ошибок, все в форматах, которые легче отслеживать, чем те, которые есть сейчас. Обещания будут иметь большое значение для создания асинхронного неблокирующего кода, который мы пишем, таким же предсказуемым и однонаправленным, как и синхронный, «быстрый» блокирующий код, который мы привыкли писать. Что довольно круто. Как мы это делаем?

Самое крутое, что обещания не полагаются ни на что, кроме базового JavaScript. Javascript теперь имеет встроенную поддержку обещаний в его коде (начиная с ES6) - вы можете создать экземпляр нового обещания в любое время, когда захотите. Но так делать не обязательно. До того, как промисы стали доступны в JavaScript, многие бесстрашные авторы JavaScript писали свои собственные библиотеки обещаний, которые вы могли установить в свою кодовую базу. Возможно, вы все равно захотите их использовать, поскольку некоторые из них более эффективны, чем обещание JS, а некоторые могут иметь дополнительные функции, которые вам пригодятся. Вот довольно солидный список из A + библиотек, соответствующих их спецификациям.

Так. Что такое обещание? Обещание - это буквально старый добрый объект.

Это ничем не отличается от любого объекта, который вы создаете сами (посмотрите этот список и посмотрите, сколько других кодировщиков сделали именно это). Это объект, созданный конструктором, который имеет множество полезных свойств и методов. По сути, есть четыре вещи, которые делают обещание обещанием:

1. Обещание требует исполнителя. Вот в чем дело. То, что нам действительно нужно сделать. Исполнитель - это какая-то функция, которую мы используем, возвращаемые результаты которой нам нужно контролировать асинхронно.

2. У обещания есть внутреннее состояние (к которому мы не можем получить доступ или изменить напрямую). Каждое обещание начинается с состояния «ожидание». Только один раз за всю «жизнь» он может измениться. Он может либо измениться с ожидающего на выполненный (ура!), Либо с ожидающего на отклоненный (ура!). После того, как состояние нашего обещания изменилось, оно не может снова стать ожидающим. Он также не может перейти от выполненного к отклоненному или от отклоненного к выполненному. У нас есть только один шанс изменить каждое обещание. Что хорошо! Состояние обещания будет определять, какие наборы функций обратного вызова мы запускаем (если они есть) (те, которые мы помечаем как случай ошибки, или те, которые мы помечаем как успешный вариант). Как только обещание находится в незавершенном состоянии, это факт, на который мы можем полагаться в течение всего будущего нашего кода.

3. Обещание имеет ценность. Это значение в случае успеха будет данными, которые нам нужны, или ошибкой (или «причиной»), если что-то пойдет не так. Внутреннее состояние обещания всегда будет разрешать в тандеме с ETIHER ошибку или данные как значение обещания, и после изменения этого состояния не только состояние остается неизменным, но и значение обещания тоже. Значение не может измениться, как только мы его получим (по крайней мере, в отношении самого обещания). Что, опять же, является желательным положением дел - мы не хотим, чтобы что-то шло у нас под ногами.

4. Наконец, чтобы обещание стало обещанием, оно должно иметь доступ к методу then (и мы поговорим об этом более подробно через секунду). Это то, что заменит вложенные обратные вызовы - это то место, где мы будем обрабатывать результаты нашего асинхронного действия.

Часть III. Обещания

Есть два основных способа встретить обещания в дикой природе. В JavaScript у нас есть доступ к конструктору обещаний, поэтому мы можем создать экземпляр нового обещания и передать его нашему исполнителю и обратным вызовам. Это немного странно, но все же намного проще, чем ванильная синхронизация, потому что она есть и будет оставаться плоской, независимо от того, сколько вещей мы связываем с ней, и любые странности довольно заранее загружены. Это также означает, что наш код является переносимым - мы можем сделать обещание в одном месте и прикрепить обработчик к нему в любом другом месте нашего кода (мы даже можем прикрепить несколько несвязанных обработчиков к одному и тому же обещанию).

Однако чаще мы видим, что обещания поддерживаются и встроены в другие библиотеки, которые использует наше приложение. Sequelize, например, который помогает нам настроить схему нашей базы данных и избавляет нас от ужасающей задачи написания наших собственных SQL-запросов, поддерживает обещания с помощью своих действий с базой данных, поэтому мы можем просто рассматривать любое из этих действий, как если бы они были переменными, представляющими обещаю (потому что они есть). Что довольно здорово.

И мы только начинаем осознавать настоящую магию обещаний. У каждого обещания есть метод под названием «then». И проще всего понять, что тогда делает, в очень простых, англоязычных терминах. Если у вас есть обещание, оно будет выполнено (то есть разрешено или отклонено), и ЗАТЕМ запустите код, который передается в качестве обратного вызова функции then. И что еще более удобно, функция, которой мы передаем, автоматически принимает в качестве аргумента полученные данные или ошибку.

Но это еще не все! Иметь в наличии только один из них - не очень хорошо. Ранее мы говорили о том, насколько взаимосвязанными могут быть различные операции, и поэтому, чтобы обещания были действительно полезными, они должны быть связаны друг с другом. Возвращаемое значение функции then само по себе является новым обещанием, поэтому следующее затем получит разрешение или ошибку от этого вышестоящего обещания в качестве аргумента.

Внезапно мы можем контролировать и то, и другое, когда наши функции выполняются (или, по крайней мере, мы можем гарантировать, что reliantNewPromise не будет работать, пока somePromise не разрешит и не передаст свое значение, и что weCouldDoThisAllDayPromise не будет работать, пока это не разрешит и т. д. и т. д.) и гарантируют, что у каждого будет информация, необходимая для эффективной работы. И снова - это не так сложно для глаз, не правда ли?

Часть IV. Мы все делаем ошибки. Или, по крайней мере, ошибки.

Одна из самых неприятных вещей в наших старых заклятых врагах, ванильных асинхронных обратных вызовах, - это формат PITA error-first. Обработка ошибок с помощью обещаний намного проще. Прежде всего, обещания имеют формат «успех прежде всего». Таким образом, один из способов обработки ошибок в цепочке обещаний - это передать нашему второму аргументу функции, чтобы наша программа знала, что делать в любом возможном результате.

Но обещания дают нам то, чего нет в асинхронных обратных вызовах - они заставляют нас пузыриться. Если одно обещание отклоняется, и то, в котором оно в настоящее время находится, не имеет обратного вызова для случая ошибки, оно перейдет к следующему затем и так далее, пока не найдет первое, то с обратным вызовом для случая ошибки. Итак, другой, более понятный способ управления ошибками в обещаниях - это иметь два отдельных терминала, с обработкой ошибок, следующей за успешной обработкой, где мы просто передаем «null» в качестве первого аргумента.

Но… становится еще лучше! Обычно есть довольно ограниченное количество вещей, которые мы хотим делать, когда возникают ошибки, верно? (Например, запишите их в консоль или отправьте в браузер, чтобы сообщить нашему клиенту, что он что-то напортачил.) Разве не было бы еще лучше, если бы мы могли обрабатывать все эти ошибки в одном месте? Благодаря этой способности пузырьков мы действительно можем добиться успеха только в том случае, если все ошибки будут попадать прямо в этот суперполезный метод обещания «catch». Это немного похоже на настройку try / catch в JavaScript - любые ошибки, возникающие в восходящем потоке, будут «перехвачены» нашим блоком catch и обработаны в его обратном вызове, что устраняет необходимость в любой другой обработке ошибок, засоряющей наш код. И, конечно же, чем меньше у нас мест для реализации чего-либо, тем меньше у нас возможностей для ошибки.

Чтобы прояснить ситуацию - промис решает, какое дело запускать, в зависимости от его внутреннего состояния. Пока состояние ожидает (навсегда, если обещание никогда не выполняется по какой-либо причине), никакие обратные вызовы не будут выполняться. Как только состояние установлено, ЛИБО обработчики успеха будут срабатывать, ИЛИ обработчики ошибок. Здесь нет золотой середины, нет обстоятельств, при которых будут работать и обработчики успеха, и обработчики ошибок. Вот почему так важно, чтобы состояние обещания могло измениться только один раз и только определенным предсказуемым образом. Для новичков в JavaScript лучшие JavaScript-фреймворки - лучший способ преуспеть.

Часть V. Итог

Если вы посмотрите на цепочки из этих примеров, вы можете начать видеть, что промисы позволяют нам управлять нашим кодом более функционально и позволяют нам добиться успеха и всплывания ошибок в чем-то вроде формы, которая у нас есть в синхронном контексте. Более того, стало намного проще управлять и понимать, к каким данным и когда у нас есть доступ. И мы не ограничены только этим конвейером непроходимых пузырей, когда мы получаем один фрагмент данных и переходим к следующему, используя или теряя стиль. У нас также есть такие методы, как Promise.all (), где мы можем агрегировать результаты нескольких обещаний и обрабатывать все данные, которые они возвращают вместе, то есть мы можем получить к ним доступ сразу, если захотим.

Давайте рассмотрим здесь небольшой пример:

Здесь у нас есть ряд обещаний, которые мы храним в массиве. Они не полагаются на успех друг друга, но мы хотим, чтобы они все вместе добивались успеха или терпели поражение. Передав этот массив методу all в JavaScript Promise, мы вернем одно обещание, которое либо разрешится, когда будет выполнено каждое обещание, либо будет отклонено с указанием причины первого отклоненного значения в массиве. Если, как мы надеемся, наш массив обещаний разрешится полностью, результирующие значения будут массивом значений, результаты которых упорядочены по порядку обещаний в исходном массиве - независимо от того, какие обещания разрешаются первыми. Разве это не здорово?

Свяжем это красивым бантом. Промисы дают нам простую цепочку вместо бесконечного вложения - наш код снова становится линейным, как нам нравится. Они обеспечивают простую и унифицированную обработку ошибок. Они делают наши асинхронные действия переносимыми - мы можем настраивать их в одном месте и управлять тем, что делать, когда они завершаются в другом, а время их выполнения неоднозначно, так что мы можем даже прикрепить к ним больше обработчиков (или цепочек обработчиков!) В любое время. или где бы мы ни хотели, все еще будучи уверенными, что каждая соответствующая функция успешной обработки или обработчика ошибок будет запущена.

Если кажется, что обещание этого прошло быстрее, чем наращивание, если обещания кажутся менее весомыми, чем использование ванильных асинхронных обратных вызовов, тогда это хорошо! Это признак успеха, поскольку они проще с точки зрения пользователя. Вот почему мы обычно хотим использовать их везде, где только возможно, вместо vanilla async.

К сожалению, обещания не решают всех наших проблем. Они по-прежнему хранят наши медленно получаемые данные в виде отдельной воронки внутри нашей программы. У нас нет простого способа сделать информацию из обработанного обещания доступной глобально в нашем приложении или гарантировать, что код не в цепочке обещаний не будет пытаться использовать наши данные обещания. до того, как это будет решено. Для этого вам придется подождать, пока я не займусь написанием статьи об async await!