В дополнение к моему предыдущему посту о том, как использовать промисы, я решил написать что-нибудь о том, как они на самом деле работают.

Обещания волшебны

Промисы — это применение монадных или функтороподобных функциональных принципов к проблеме организации кода.

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

Ключевые особенности конструкции обещания:

  • упаковка отложенного значения
  • четко определенный контроль перехода состояний
  • компонуемость и совместимость с другими обещаниями
  • сглаживание кода
  • прямое распространение ошибок

За занавесом…

Для реализации обещания нужно:

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.