В дополнение к моему предыдущему посту о том, как использовать промисы, я решил написать что-нибудь о том, как они на самом деле работают.
Обещания волшебны
Промисы — это применение монадных или функтороподобных функциональных принципов к проблеме организации кода.
Обещания показывают, как с помощью правильного факторинга и правильных примитивных концепций реализация сложного поведения может быть значительно упрощена.
Ключевые особенности конструкции обещания:
- упаковка отложенного значения
- четко определенный контроль перехода состояний
- компонуемость и совместимость с другими обещаниями
- сглаживание кода
- прямое распространение ошибок
За занавесом…
Для реализации обещания нужно:
1. Конечный автомат
Для обеспечения взаимодействия между библиотеками промисов появился стандарт реализации под названием Promises/A+.
Promises/A+ определяет API, определенные части логики промисов, допустимый набор переходов и связанное с ними поведение.
Допустимые переходы:
ожидание => выполнено (со значением)
ожидание => отклонено (с указанием причины)
Конечный автомат включен в конце этого поста.
2. Способ планирования задания в очереди заданий
Следующая функция оборачивает функцию, чтобы она выполнялась асинхронно. Здесь мы используем `setTimeout` (макрос) для асинхронности.
function async(cb) { return (...args)=>setTimeout(()=>cb(...args)); }
В «реальной жизни» WHATWG требует, чтобы промисы фактически обслуживались как микрозадачи, что дает нативным промисам в Интернете другой приоритет выполнения.
3. Батут
батут — это просто причудливое название цикла, который итеративно вызывает функции, возвращающие преобразователь. Thunk — это сгенерированная программой функция для поддержки оценки другого.
В нашей реализации промиса мы создаем переходники, соответствующие вызовам «затем», которые будут координировать выполнение логики «затем».
const go = async(function(thens, result) { while (thens.length) { thens.pop()(result); } });
4. Конструктор
Конструктор промиса устанавливает начальное состояние и немедленно и синхронно запускает функцию-исполнитель.
«Машина» — это конечный автомат, управляющий переходами между состояниями промисов.
function Promise(executor = ()=>{}) { const thens = []; const p = { then, catch: ccatch, // `catch` is a reserved keyword in JavaScript resolve, reject, }; machine.transition(p, states.pending); executor(resolve, reject); return p; // `resolve`, `reject`, `then` and `ccatch` go in here... }
5. Функция разрешения (и отклонения)
Эти функции запускают переход состояния и обеспечивают продолжение действия цепочки промисов вызовом батута. Функция отклонения аналогична и здесь не показана.
function resolve(result) { machine.transition(p, states.fulfilled, result); go(thens, result); }
6. Функция then (и catch)
Это сердце реализации обещания. Это обеспечивает компонуемость, ветвление и отложенное переключение от оболочки к результату.
Реализация catch не показана.
Далее следует упрощенная (но жизнеспособная для дальнейшего развития) реализация «тогда». Обработка ошибок не включена.
function then(cb) { const p = new Promise(); thens.unshift(inboundValue => { const result = cb(inboundValue); if (result && result.then) { p[‘__isResolved__’] = true; result.then(subResult => { p.resolve(subResult); return subResult; }); return; } p.resolve(result) }); return p; }
Обратите внимание, что массив `then` в сочетании с возвратом нового промиса при каждом вызове `then`, а также связывание результата под-обещаний через `then` необходимы для включения поведение ветвления требуется как часть Promises/A+.
Это пример ветвления:
const p = new Promise(() => {}); p.then(result=>'a').then(console.log); // a p.then(result=>'b').then(console.log); // b
Примечания
– Пожалуй, самый важный метод – это «тогда».
- `then` обеспечивает логику для переноса будущего значения.
– Отложенное разрешение обеспечивается асинхронным батутом.
– Структура данных очереди («тогда») используется для поведения «первым пришел – первым обслужен». то есть первое предоставленное `then` является первым оцениваемым.
– Спецификация Promise/A+ представляет собой управляемую сообществом спецификацию функциональной совместимости обещаний и определяет разрешенные переходы состояний.
- `then` обеспечивает возможность компоновки, реализуя специальную логику обработки для возвращаемых значений типа «promise».
– Связующая природа обещания API сглаживает код, который в противном случае был бы сильно вложенным, и упрощает распространение ошибок.
- Реализация обработки ошибок ("reject", "catch") в промисах похожа на логику "resolve" и "then" соответственно, и я опустил ее здесь для простоты.
– Реализация промисов, определенная в этом посте, упрощена, чтобы облегчить изложение, и хотя ее можно использовать в качестве основы для разработки совместимой реализации, она не соответствует всем деталям Promises/A+.
Резюме
Несмотря на то, что потребовалось более пятнадцати лет, чтобы они были приняты основным сообществом JavaScript, обещания показывают, что с применением функциональных принципов, которым уже несколько десятков лет, изменения в базовом языке не являются необходимыми для поддержки упрощения кода для понимания и обслуживания.
Государственный аппарат
const states = { 'pending': 'pending', 'rejected': 'rejected', 'fulfilled': 'fulfilled', }; const machine = { transition }; const uninitializedToPending = { from: states.uninitialized, to: states.pending, action: uninitializedToPendingAction }; const pendingToFulfilled = { from: states.pending, to: states.fulfilled, action: pendingToFulfilledAction }; const pendingToRejected = { from: states.pending, to: states.rejected, action: pendingToRejectedAction }; const transitions = [ uninitializedToPending, pendingToFulfilled, pendingToRejected ]; function transition(promise, to, ...args) { const transition = transitions // A map would be faster .find(t => t.from === promise['status'] && t.to === to); if (!transition) { throw 'invalid transition'; } return transition.action(promise, ...args); } function uninitializedToPendingAction(p) { p['__status__'] = 'pending'; p['__isResolved__'] = false; return p; } function pendingToFulfilledAction(p, result) { p['__status__'] = 'fulfilled'; p['__isResolved__'] = true; p['__result__'] = result; return p; } function pendingToRejectedAction(p, reason) { p['__status__'] = 'rejected'; p['__isResolved__'] = true; p['__result__'] = reason; return p; } export default machine;
Меня зовут Бен Астон, и спасибо, что прочитали мой пост сегодня. Я лондонский консультант по JavaScript, доступный для краткосрочных консультаций по всему миру. Со мной можно связаться по адресу [email protected].
Если вам понравилась эта часть, пожалуйста, дайте мне знать, нажав кнопку «рекомендовать» ниже.
Вас также может заинтересовать мой пост о замыканиях в JavaScript.