Translate

Поиск по этому блогу

четверг, 10 мая 2018 г.

Интервью JavaScript: Что такое Promise?

«Мастер интервью с JavaScript» представляет собой серию сообщений, предназначенных для подготовки кандидатов на общие вопросы, с которыми они могут столкнуться при подаче заявки на позицию JavaScript на среднем и старшем уровнях (a mid to senior-level JavaScript position). Это вопросы, которые я часто использую в реальных интервью.




Что такое Promise?

Обещание (Promise)- это объект, который может произвести одно значение в будущем: либо разрешенное значение, либо причина, по которой он не разрешен (например, произошла сетевая ошибка).

Обещание может быть в одном из трех возможных состояний: выполнено- fulfilled, отклонено - rejected или ожидается- pending. Пользователи Promise могут присоединять обратные вызовы для обработки выполненного значения или причины отклонения.

Обещания нетерпеливы, а это означает, что обещание начнет выполнять любую задачу, которую вы даете ему, как только вызывается конструктор обещаний. Если вам нужно лениться, проверьте observables или tasks.

Неполная история Promise - обещаний

Раннее, реализации обещаний и фьючерсов (аналогичная / смежная идея) начали появляться в таких языках, как MultiLisp и Concurrent Prolog еще в 1980-х годах. Использование слова Promise - «обещание» было придумано Барбарой Лисковой и Любой Шририа в 1988 году.

В первый раз, когда я услышал о обещаниях в JavaScript, Node был совершенно новым, и сообщество обсуждало лучший способ обработки асинхронного поведения. Сообщество какое-то время экспериментировало с обещаниями, но в конечном итоге решилось на Node-standard error-first callbacks (стандарт Ноды с ошибкой первого обратного вызова).

Примерно в то же время Dojo добавили обещания через Deferred API. Растущий интерес и активность в конечном итоге привели к недавно сформированной спецификации Promises / A, призванной сделать различные обещания более интероперабельными.

Асинхронное поведение jQuery было реорганизовано вокруг обещаний (Promise). Поддержка обещаний (Promise) jQuery имела большое сходство с Dojo's Deferred, и она быстро стала наиболее часто используемой реализацией (Promise) обещаний в JavaScript из-за огромной популярности jQuery - на то время. Однако, там не было поддержки двухканального (выполненное / отклоненное) поведения цепочки и управления исключениями, на которое люди рассчитывали при создании инструментов поверх обещаний (Promise).

Несмотря на эти недостатки, jQuery официально сделал JavaScript обещания (Promise) мейнстримом, и лучше автономные библиотеки обещаний, таких как Q, When, и Bluebird стали очень популярными. Несовместимость реализации jQuery привела к некоторым важным разъяснениям в спецификации обещаний, которая была переписана и переименована как спецификация Promises / A +.

ES6 принесла Promise / A + совместимый глобальный Promise, и некоторые очень важные API были построены поверх новой стандартной поддержки Promise: особенно спецификация WHATWG Fetch (ссылка заблокирована в России-> use VPN - пр. переводчика) и стандарт Async Functions (проект на 3 стадии на момент написания этой статьи).

Promises - обещаний, описанные здесь, это промисы которые совместимы с спецификациями Promises / A +, с акцентом на стандартную реализацию Promise ECMAScript.

Как работают promis?

Обещание (promis)- это объект, который можно синхронно возвращать из асинхронной функции. Он будет в одном из трех возможных состояний:

  1. Fulfilled(выполнено): onFulfilled() будет вызвано (например: reject() был вызван)
  2. Rejected (отклонено): onRejected() будет вызвано (например: reject() был вызван)
  3. Pending (ожидает): еще не выполнено или отклонено.


Обещание (promis) разрешается - settled, если оно не ожидается (оно было разрешено или отклонено - resolved or rejected). Иногда люди используют resolved и settled как то же самое: not pending..

После settled обещание не может быть resettled. Вызов resolve () или reject () снова не будет иметь эффекта. Важнейшей особенностью является неизменность обещанного обещания (promis).

Собственные обещания JavaScript не раскрывают состояния обещаний. Вместо этого вы должны рассматривать обещание как черный ящик. Только функция, ответственная за создание обещания, будет обладать знанием статуса обещания (promise status) или доступа к разрешению или отказу (resolve or reject).

Вот функция, которая возвращает обещание, которое будет разрешено после заданной задержки:

const wait = time => new Promise((resolve) => setTimeout(resolve, time));

wait(3000).then(() => console.log('Hello!')); // 'Hello!'


Пример кода на CodePen

Наш вызов wait (3000) будет ждать 3000 мс (3 секунды), а затем выведет в консоль «Hello!». Все спецификации, совместимые с обещаниями, определяют метод .then (), который вы используете для передачи обработчиков, которые могут принимать разрешенные или отклоненные значения.

Конструктор обещаний ES6 выполняет функцию. Эта функция принимает два параметра: resolve () и reject (). В приведенном выше примере мы используем только метод resolve (), поэтому я оставил reject () в списке параметров. Затем мы вызываем setTimeout () для создания задержки и вызываем resolve (), когда время ожидания закончено.

Вы можете опционально resolve () или reject () со значениями, которые будут переданы на функции обратного вызова, связанные с .then ().

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

Важные правила обещания (promise)

Стандарт для обещаний был определен сообществом Promises / A + Существует множество реализаций, которые соответствуют стандарту, включая общие требования стандарта ECMAScript для JavaScript.

Обещания следующей спецификации должны соответствовать определенному набору правил:
  1. Обещание или «thenable» - это объект, который поставляет стандартный метод .then ().
  2. Ожидающее обещание (pending promise) может перейти в отработанное (fulfilled) или отвергнутое (rejected) состояние.
  3. уже установленное состояние fulfilled - исполненное или rejected - отвергнутое. не должно и не может меняться. переходить в другие состояния.
  4. Однажды установленное состоянеи должно иметь значение ( оно может быть и undefined). Это значение не должно меняться.
Изменение в этом контексте относится к сравнению идентичности (===). Объект может использоваться как выполненное значение, а свойства объекта могут мутировать.

Каждое обещание должно предоставить метод .then () со следующей подписью:

promise.then(
  onFulfilled?: Function,
  onRejected?: Function
) => Promise



Метод .then () должен соответствовать следующим правилам:
  1. И onFulfilled (), и onRejected () являются необязательными.
  2. Если предоставленные аргументы не являются функциями, они должны быть проигнорированы.
  3. onFulfilled () будет вызван после того, как обещание будет исполнено, причем значение обещания станет первым аргументом.
  4. onRejected () будет вызываться после того, как обещание будет отклонено, и причина отказа в качестве первого аргумента. Причиной может быть любое допустимое значение JavaScript, но поскольку отклонения по существу являются синонимом исключений, я рекомендую использовать объекты Error.
  5. Ни onFulfilled (), ни onRejected () не могут быть вызваны более одного раза.
  6. .then () можно вызвать много раз по тому же обещанию. Другими словами, обещание может быть использовано для агрегирования обратных вы
  7. .then () должен вернуть новое обещание, promise2.
  8. Если onFulfilled () или onRejected () возвращают значение x, а x является обещанием, promise2 будет блокироваться (предполагать то же состояние и значение, что и x). В противном случае promise2 будет выполнено со значением x.
  9. Если либо onFulfilled, либо onRejected выбрасывает исключение e, promise2 должен быть rejected (отклонен) вместе с e в качестве причины.
  10. Если onFulfilled не является функцией и promise1 выполняется, promise2 должно выполняться с тем же значением, что и promise1.
  11. Если onRejected не является функцией и promise1 отвергается- rejected, promise2 должен быть отклонен по той же причине, что и promise1.


Цепочка обещаний

Именно потому, что .then () всегда возвращает новое обещание, можно связать обещания с точным контролем над тем, как и где обрабатываются ошибки. Обещания позволяют вам имитировать поведение обычного / синхронного кода try / catch.

Подобно синхронному коду, цепочка приведет к последовательности, которая выполняется в последовательном порядке. Другими словами, вы можете сделать:

fetch(url)
  .then(process)
  .then(save)
  .catch(handleErrors)
;



Предполагая, что каждая из функций, fetch (), process () и save () возвращают обещания, process () будет ждать завершения функции fetch () перед запуском, а save () будет ждать завершения process () перед запуском. handleErrors () будет работать только в том случае, если любое из предыдущих обещаний отклоняется.

Вот пример сложной цепочки обещаний с множественными отказами:

const wait = time => new Promise(
  res => setTimeout(() => res(), time)
);

wait(200)
  // onFulfilled() can return a new promise, `x`
  .then(() => new Promise(res => res('foo')))
  // the next promise will assume the state of `x`
  .then(a => a)
  // Above we returned the unwrapped value of `x`
  // so `.then()` above returns a fulfilled promise
  // with that value:
  .then(b => console.log(b)) // 'foo'
  // Note that `null` is a valid promise value:
  .then(() => null)
  .then(c => console.log(c)) // null
  // The following error is not reported yet:
  .then(() => {throw new Error('foo');})
  // Instead, the returned promise is rejected
  // with the error as the reason:
  .then(
    // Nothing is logged here due to the error above:
    d => console.log(`d: ${ d }`),
    // Now we handle the error (rejection reason)
    e => console.log(e)) // [Error: foo]
  // With the previous exception handled, we can continue:
  .then(f => console.log(`f: ${ f }`)) // f: undefined
  // The following doesn't log. e was already handled,
  // so this handler doesn't get called:
  .catch(e => console.log(e))
  .then(() => { throw new Error('bar'); })
  // When a promise is rejected, success handlers get skipped.
  // Nothing logs here because of the 'bar' exception:
  .then(g => console.log(`g: ${ g }`))
  .catch(h => console.log(h)) // [Error: bar]
;


Посмотреть на Codepen

Обработка ошибок

Обратите внимание, что обещания имеют как успешное завершение, так и обработчик ошибок, и очень часто можно увидеть код, который делает это:

save().then(
  handleSuccess,
  handleError
);



Но что произойдет, если handleSuccess () выдает ошибку? Обещание, возвращенное из .then (), будет отклонено, но нет ничего, чтобы поймать отказ - это означает, что ошибка в вашем приложении проглатывается. К сожалению!

По этой причине некоторые люди считают, что приведенный выше код является анти-шаблоном, и вместо этого рекомендуют следующее:


save()
  .then(handleSuccess)
  .catch(handleError)
;



Разница тонкая, но важная. В первом примере ошибка, возникающая в операции save (), будет обнаружена, но ошибка, возникающая в функции handleSuccess (), будет проглатываться.


Без .catch () ошибка в обработчике успеха не реализована.
Во втором примере .catch () будет обрабатывать отклонения от save () или handleSuccess ().


С .catch () обрабатываются оба источника ошибок. (источник диаграммы)

Конечно, ошибка save () может быть сетевой ошибкой, тогда как ошибка handleSuccess () может быть вызвана тем, что разработчик забыл обработать определенный код состояния. Что делать, если вы хотите обращаться с ними по-другому? Вы можете выбрать для них обоих:

save()
  .then(
    handleSuccess,
    handleNetworkError
  )
  .catch(handleProgrammerError)
;



Независимо от того, что вы предпочитаете, я рекомендую прекратить все цепочки обещаний с помощью .catch (). Это стоит повторить:
I recommend ending all promise chains with a .catch().



Как отменить обещание?

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

Вот некоторые распространенные ошибки, которые люди совершают, когда откатываются от обещания:

Добавление .cancel () к обещанию

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

Забыли почистить.

367/5000 Некоторые умные люди поняли, что есть способ использовать Promise.race () как механизм отмены. Проблема заключается в том, что контроль отмены берется из функции, которая создает обещание, которое является единственным местом, где вы можете проводить правильные действия по очистке, такие как очистка тайм-аутов или освобождение памяти путем очистки ссылок на данные и т. д.

забыли обработать reject отклоненного обещания

Знаете ли вы, что Chrome бросает предупреждающие сообщения по всей консоли, когда вы забываете обработать отказ от обещаний? К сожалению!

Слишком сложно

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

Вы хотите переключиться в зависимости от того, есть ли исключение или отмена? Да, конечно. Это работа обещания? По-моему, нет, это не так.

Переосмысление отмены обещаний

Как правило, я передаю всю информацию, которую обещание необходимо определить, как разрешить / отклонить / отменить на время создания обещания. Таким образом, нет необходимости в методе .cancel () по обещанию. Возможно, вам будет интересно узнать, как вы могли бы узнать, собираетесь ли вы отменить время обещания.

«Если я еще не знаю, отменить или нет, как я узнаю, что передать, когда я создам обещание?»


Если бы существовал какой-то объект, который мог бы выдерживать потенциальную ценность в будущем ... о, подождите.

Значение, которое мы передаем, чтобы представлять, отменить или нет, может быть обещанием. Вот как это может выглядеть:


const wait = (
  time,
  cancel = Promise.reject()
) => new Promise((resolve, reject) => {
  const timer = setTimeout(resolve, time);
  const noop = () => {};

  cancel.then(() => {
    clearTimeout(timer);
    reject(new Error('Cancelled'));
  }, noop);
});

const shouldCancel = Promise.resolve(); // Yes, cancel
// const shouldCancel = Promise.reject(); // No cancel

wait(2000, shouldCancel).then(
  () => console.log('Hello!'),
  (e) => console.log(e) // [Error: Cancelled]
); 


Посмотреть на CodePen

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

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

Примечание. Возможно, вам интересно, для чего предназначена функция noop (). Слово noop означает no-op, что означает функцию, которая ничего не делает. Без него V8 будет вызывать предупреждения: UnhandledPromiseRejectionWarning: Отказ от необработанного обещания. Это хорошая идея, чтобы всегда обрабатывать отклонения обещаний, даже если ваш обработчик является noop ().


Абстрагированное Отклонение обещаний

Это нормально для таймера wait (), но мы можем абстрагировать эту идею дальше, чтобы инкапсулировать все, что вам нужно запомнить:
  1. Отклонить обещание отмены по умолчанию - мы не хотим отменять или бросать ошибки, если не будет отменено обещание отмены.
  2. Не забудьте выполнить очистку, когда вы отклоняете для отмены.
  3. Помните, что очистка onCancel сама может выдать ошибку, и эта ошибка также потребует обработки. (Обратите внимание, что обработка ошибок в примере ожидания отсутствует - легко забыть!)


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


speculation(fn: SpecFunction, shouldCancel: Promise) => Promise



Функция SpecFunction аналогична функции, которую вы передали бы в конструктор Promise, за одним исключением: для него используется обработчик onCancel ():


SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void






// HOF Wraps the native Promise API
// to add take a shouldCancel promise and add
// an onCancel() callback.
const speculation = (
  fn,
  cancel = Promise.reject() // Don't cancel by default
) => new Promise((resolve, reject) => {
  const noop = () => {};

  const onCancel = (
    handleCancel
  ) => cancel.then(
      handleCancel,
      // Ignore expected cancel rejections:
      noop
    )
    // handle onCancel errors
    .catch(e => reject(e))
  ;

  fn(resolve, reject, onCancel);
});


GitHub

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

Я реализовал поддерживаемую производственную версию этого с красными случаями, рассматриваемыми как библиотека с открытым исходным кодом, Speculation.

Давайте используем улучшенную абстракцию библиотеки для перезаписи утилиты waitable () для отмены. Сначала установите спекуляцию:


   npm install --save speculation



Теперь вы можете импортировать и использовать это:


import speculation from 'speculation';

const wait = (
  time,
  cancel = Promise.reject() // By default, don't cancel
) => speculation((resolve, reject, onCancel) => {
  const timer = setTimeout(resolve, time);

  // Use onCancel to clean up any lingering resources
  // and then call reject(). You can pass a custom reason.
  onCancel(() => {
    clearTimeout(timer);
    reject(new Error('Cancelled'));
  });
}, cancel); // remember to pass in cancel!

wait(200, wait(500)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // 'Hello!'

wait(200, wait(50)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // [Error: Cancelled]


GitHub wait-speculation.js

Это немного упрощает, потому что вам не нужно беспокоиться о noop (), ловя ошибки в вашей функции onCancel (), функции или других случаях. Эти детали были отвлечены speculation (). Проверьте это и не стесняйтесь использовать его в реальных проектах.

Дополнительные возможности Native JS Promise

У родного объекта Promise есть некоторые дополнительные вещи, которые могут вас заинтересовать:
  • Promise.reject() - возвращает отклоненное обещание.
  • Promise.resolve() - возвращает разрешенное обещание.
  • Promise.race() - берет массив (или любой итерабельный) и возвращает обещание, которое разрешает со значением первого разрешенного обещания в итерируемом, или отклоняет по причине первого обещания, которое отвергается.
  • Promise.all() - берет массив (или любой итерабельный) и возвращает обещание, которое разрешает, когда все обещания в итерируемом аргументе разрешаются, или отклоняется по причине первого обещанного обещания, которое отклоняет.


Заключение

Обещания стали неотъемлемой частью нескольких идиом в JavaScript, включая стандарт WHATWG Fetch, используемый для большинства современных запросов ajax, и стандарт Async Functions, используемый для создания синхронного асинхронного кода.

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

Например, если вы используете Redux, я предлагаю вам проверить redux-saga: библиотека, используемая для управления побочными эффектами в Redux, которая зависит от асинхронных функций в документации.

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

Исследуйте серию



Барбара Лисков; Люба Шрира (1988). «Обещания: лингвистическая поддержка эффективных асинхронных вызовов процедур в распределенных системах». Материалы конференции SIGPLAN '88 по программированию и внедрению языка программирования; Атланта, Джорджия, США, стр. 260-267. ISBN 0-89791-269-1, опубликованный ACM. Также опубликовано в ACM SIGPLAN Notices, том 23, выпуск 7, июль 1988 г.







Автор: Eric Elliott .
Перевод: Kolesnikov Yaroslav



                                                                                                                                                             

Комментариев нет:

Отправить комментарий



Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.Пишите мне - kolesnikovy70 почта gmail.com