Интерпретация принципа внутренней реализации Promise

внешний интерфейс JavaScript Promise
Интерпретация принципа внутренней реализации Promise

предисловие

ранееPromiseСинтаксис Боуэн написал, но только начальный уровень, мало еды и безвкусно. Всегда хотел написать ответPromiseОсознал, что из-за ограниченного понимания я много раз не был удовлетворен написанным. Откладывалось и откладывается, по сей день.

вместе сСпецификация Обещание/A+,Спецификация ECMAscriptправильноPromiseAPI сформулирован и выполнен, и базовая единица асинхронной операции Javascript постепенно изменилась сcallbackПреобразование вpromise. подавляющее большинствоJavaScript/DOMНовый асинхронный API платформы (Fetch,Service worker) также основаны наPromiseпостроен. этой парыPromiseПонимание — это не то, что можно полностью понять, просто взглянув на API и прочитав несколько практик. Таким образом, автор анализирует детали, растет вместе с читателями и движется вперед.

Эта статья является второй частью серии практических решений для внешнего интерфейса асинхронного программирования, основной анализPromiseВнутренний механизм и принцип реализации. Последующие асинхронные серии также будут включатьGenerator,Async/AwaitСвязанные, выкопайте яму, чтобы занять место.

Примечание: эта статьяPromiseподчинятьсяPromises/A+спецификация, ссылка на реализациюthen/promise.

Promise

A promise represents the eventual result of an asynchronous operation. --Promises/A+

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation. --ECMAscript

Promises/A+ECMAscript

PromisecallbackОбработка асинхронных обратных вызовов может привести к преждевременным вызовам, слишком поздним вызовам, слишком большому или слишком малому количеству вызовов, поглощению возможных ошибок или исключений и т. д. Кроме того, обещают принять только в первый разresolve(..)илиreject(..)Разрешение, обещает, что не изменится после изменения состояния, обещает, что все прошлоthen(..)Зарегистрированные обратные вызовы всегда вызываются асинхронно по очереди, обещая, что все исключения всегда будут перехвачены и выброшены. Она надежное обещание.

Строго говоря,PromiseЭто простой в использовании механизм для инкапсуляции и объединения будущих значений, реализации разделения задач, асинхронного управления потоком, всплытия исключений, последовательного/параллельного управления и т. д.

Примечание: упоминается в текстеcallbackПодробнее о проблеме см. в главах > 2.3, 3.3.

Стандартная интерпретация

Promise A+Спецификация короткая и лаконичная, но читайте внимательно, есть еще несколько моментов, требующих внимания.

пригодный для использования объект

thenableэто определениеthen(..)метод объекта или функции.thenableОбъект существует, чтобы сделатьPromiseреализация является более общей, поскольку она предоставляетPromise/A+нормативныйthen(..)метод. также будет следоватьPromise/A+Канонические реализации могут хорошо сосуществовать с менее каноническими, но пригодными для использования реализациями.

Идентифицироватьthenableили вести себя какPromiseОбъект может быть основан на том, имеет ли онthen(..)способ судить, который на самом деле называетсяпроверка типовтакже называемыйутиные дебаты(duck typing). дляthenableОбнаружение типа утки значения примерно похоже на:

if ( p !== null && 
     (
       typeof p === 'object' || 
       typeof p === 'function'
     ) &&
     typeof p.then === 'function'
) {
    // thenable
} else {
    // 非 thenable 
}
затем обратный вызов выполняется асинхронно

Как мы все знаем,PromiseФункция, переданная при создании экземпляра, будет выполнена немедленно,then(...)Обратные вызовы необходимо вызывать асинхронно и откладывать. Что касается того, почему звонок задерживается, я объясню позже. Здесь есть важный момент, время асинхронного вызова функции обратного вызова.

onFulfilled or onRejected must not be called until the execution context stack contains only platform code --Promise/A+

сокращенноonFulfilledилиonRejectedНепосредственно перед вызовом среды выполнения, когда стек содержит только код платформы. Слегка озадаченный,Спецификация Обещание/A+Это предложение также поясняется: «На практике необходимо обеспечить, чтобыonFulfilledа такжеonRejectedметод выполняется асинхронно и должен бытьthenВыполняется в новом стеке выполнения после цикла обработки событий, в котором был вызван метод. Эта очередь событий может использоватьмакрозадачамеханизм илимикрозадачамеханизм достижения. "

несмотря на то чтоPromise A+прямо не указаноmicrotaskещеmacrotaskвстать в очередь, ноECMAScriptВ спецификации четко указаноPromiseдолжен начинаться сPromise JobДобавлено какjob queues(он же микрозадача).Job QueueЭто новая концепция, предложенная в ES6, построенная на очереди цикла событий.job queueОн также существует для выполнения некоторых асинхронных операций с малой задержкой.

Постучите по доске, чтобы сосредоточиться, обратите внимание на этоmacrotask microtaskПредставляют две категории асинхронных задач соответственно. При приостановке задач движок JS разделит все задачи на две очереди по категориям.macrotaskочередь (также называемаяtask queue), возьмите первую задачу в очереди микрозадач и выполните ее последовательно; затем возьмите задачу макрозадачи и повторяйте, пока не будут удалены все задачи в двух очередях.

дляmicrotaskвремя исполнения,спецификация HTML whatwgЭто также объясняется в деталях, пожалуйста, нажмите, чтобы просмотреть. Дополнительные статьи по теме см. в приложенииevent loop.

Посмотрите на другой пример, чтобы углубить ваше понимание:

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
  console.log('promise1');
}).then(function () {
  console.log('promise2');
});

заказ печати? Правильный ответ:promise1, promise2, setTimeout.

в дальнейшей реализацииPromiseПеред объектом просто смоделируйте функцию асинхронного выполнения для последующего использования.PromiseИспользование обратного вызова (можно также использоватькак можно скорее библиотекаЖдать).

var asyncFn = function () {
  if (typeof process === 'object' && process !== null && 
      typeof(process.nextTick) === 'function'
  ) {
    return process.nextTick;
  } else if (typeof(setImmediate) === 'function') {
    return setImmediate;
  }
  return setTimeout;
}();
Состояние обещания

PromiseДолжно быть одно из следующих трех состояний: состояние ожидания (Pending), состояние выполнения (Fulfilled) и состояние отказа (Rejected). однаждыPromiseодеялоresolveилиreject, больше не может быть перенесен в любое другое состояние (т.е. состояниеimmutable).

Чтобы код оставался ясным, в настоящее время нет обработки исключений. В то же время для удобства выражения соглашение выглядит следующим образом:

  • выполненное использование разрешено вместо этого
  • вместо onFulfilled используйте onResolved

Конструктор обещаний

Начиная с конструктора, мы шаг за шагом добиваемсяPromsie A+нормативныйPromise.大概描述下,PromiseЧто должен сделать конструктор.

  1. инициализацияPromiseусловие(pending)
  2. инициализацияthen(..)Регистрация обратных вызовов для обработки массивов (thenметод может быть использован тем жеpromiseзвонил несколько раз)
  3. Немедленно выполнять входящиеfnфункция, пройти вPromiseвнутреннийresolve,rejectфункция
  4. ...
function Promise (fn) {
  // 省略非 new 实例化方式处理
  // 省略 fn 非函数异常处理

  // promise 状态变量
  // 0 - pending
  // 1 - resolved
  // 2 - rejected
  this._state = 0;
  // promise 执行结果
  this._value = null;
 
  // then(..) 注册回调处理数组
  this._deferreds = [];

  // 立即执行 fn 函数
  try {
    fn(value => {
      resolve(this, value);
    },reason => {
      reject(this, reason);
    })
  } catch (err) {
    // 处理执行 fn 异常
    reject(this, err);
  }
}

_stateа также_valueПеременные легко понять,_deferredsЧто делают переменные? Описание спецификации:thenметоды могут быть использованы одним и тем жеpromiseЗвонили несколько раз. Чтобы удовлетворить несколько вызововthenЗарегистрировать обработку обратного вызова, внутренне выбранную для использования_deferredsМассивы хранят объекты дескрипторов. Для конкретной структуры объекта обработки см.thenглаву функции.

окончательное исполнениеfnФункции и вызовыpromiseвнутренний частный методresolveа такжеreject.resolveа такжеrejectВнутренние детали вводятся позже.

затем функция

Promise A+Упомяните, что спецификация ориентирована на предоставление универсальногоthenметод.thenметоды могут быть использованы одним и тем жеpromiseВызывается несколько раз, каждый раз возвращая новыйpromiseобъект .thenМетод принимает два параметраonResolved,onRejected(необязательный). существуетpromiseодеялоresolveилиrejectпосле всегоonResolvedилиonRejectedФункции должны вызываться в том порядке, в котором они были зарегистрированы, а количество вызовов не должно превышать одного.

Согласно вышеизложенному,thenСхема выполнения функции примерно следующая:

  1. создать пустой экземплярpromiseВозвращается к объекту (проведениеthenцепной звонок)
  2. структураthen(..)зарегистрировать структуру обработчика обратного вызова
  3. судить о текущемpromiseусловие,pendingОбъекты ленивой обработки хранилища состоянийdeferred,Нетpendingгосударственное исполнениеonResolvedилиonRejectedПерезвоните
  4. ...
Promise.prototype.then = function (onResolved, onRejected) {

  var res = new Promise(function () {});
  // 使用 onResolved,onRejected 实例化处理对象 Handler
  var deferred = new Handler(onResolved, onRejected, res);

  // 当前状态为 pendding,存储延迟处理对象
  if (this._state === 0) {
    this._deferreds.push(deferred);
    return res;
  }

  // 当前 promise 状态不为 pending
  // 调用 handleResolved 执行onResolved或onRejected回调
  handleResolved(this, deferred);
  
  // 返回新 promise 对象,维持链式调用
  return res;
};

Handlerхранение инкапсуляции функцийonResolved,onRejectedфункции и новое поколениеpromiseобъект.

function Handler (onResolved, onRejected, promise) {
  this.onResolved = typeof onResolved === 'function' ? onResolved : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}

Почему цепочка вызовов возвращает новое обещание

Как мы понимаем, для гарантииthenцепочка функций,thenнужно вернутьсяpromiseпример. а зачем возвращать новыйpromise, вместо прямого возвратаthisТекущий объект? См. следующий пример кода:

var promise2 = promise1.then(function (value) {
  return Promise.reject(3)
})

еслиthenвыполнение функции возвращаетthisвызвать сам объект, затемpromise2 === promise1,promise2Статус также должен быть равенpromise1Такой же какresolved. а такжеonResolvedСтатус возврата в обратном вызовеrejectedобъект. учитываяPromiseзаявить один разresolvedилиrejectedбольше не может быть перенесен, так что здесьpromise2Невозможно преобразовать его в функцию возврата функции обратного вызова.rejectedСостояние возникает конфликт.

handleResolvedФункциональная функция основана на текущейpromiseсостояние, выполняемое асинхронноonResolvedилиonRejectedПерезвоните. потому чтоresolveилиrejectСвязанные функции также требуются внутри функции, которая извлекается как отдельный модуль. Прокрутите вниз, чтобы просмотреть.

разрешающая функция

PromiseВыполняет переданное немедленно при создании экземпляраfnфункцию, при этом проходя внутреннююresolveфункционировать как параметр для измененияpromiseусловие.resolveЛогика упрощенной версии функции, наверное, такова: судить и менять текущуюpromiseсостояние, хранениеresolve(..)изvalueстоимость. Определить, существует ли он в настоящее времяthen(..)Зарегистрируйте функцию выполнения обратного вызова, если она существует, она будет выполняться асинхронно последовательноonResolvedПерезвоните.

Но как начало текстаthenableописание главы, сделатьPromiseРеализация является более общей, когдаvalueсуществоватьthen(..)методthenableобъект, который необходимо сделатьPromise Resolution Procedureобработка, спецификация описывает это как[[Resolve]](promise, x). (xто есть позадиvalueпараметр).

Конкретный логический поток обработки выглядит следующим образом:

  • еслиpromiseа такжеxуказать на тот же объект, что иTypeErrorотказ от исполненияpromise

  • еслиxдляPromise, затем сделайтеpromiseприниматьxположение дел

  • еслиxкак объект или функция

    1. Пучокx.thenназначить наthen
    2. Если взятьx.thenвыдает ошибку e , отклоняет на основании epromise
    3. еслиthenэто функция, которая будетxНазовите это как область действия функции this .
    4. еслиxне является объектом или функцией, сxвыполнить для параметровpromise

исходная ссылкаPromise A+Технические характеристикиPromise Resolution Procedure.

function resolve (promise, value) {
  // 非 pending 状态不可变
  if (promise._state !== 0) return;
  
  // promise 和 value 指向同一对象
  // 对应 Promise A+ 规范 2.3.1
  if (value === promise) {
    return reject( promise, new TypeError('A promise cannot be resolved with itself.') );
  }
  
  // 如果 value 为 Promise,则使 promise 接受 value 的状态
  // 对应 Promise A+ 规范 2.3.2
  if (value && value instanceof Promise && value.then === promise.then) {
    var deferreds = promise._deferreds
    
    if (value._state === 0) {
      // value 为 pending 状态
      // 将 promise._deferreds 传递 value._deferreds
      // 偷个懒,使用 ES6 展开运算符
      // 对应 Promise A+ 规范 2.3.2.1
      value._deferreds.push(...deferreds)
    } else if (deferreds.length !== 0) {
      // value 为 非pending 状态
      // 使用 value 作为当前 promise,执行 then 注册回调处理
      // 对应 Promise A+ 规范 2.3.2.2、2.3.2.3
      for (var i = 0; i < deferreds.length; i++) {
        handleResolved(value, deferreds[i]);
      }
      // 清空 then 注册回调处理数组
      value._deferreds = [];
    }
    return;
  }

  // value 是对象或函数
  // 对应 Promise A+ 规范 2.3.3
  if (value && (typeof value === 'object' || typeof value === 'function')) {
    try {
      // 对应 Promise A+ 规范 2.3.3.1
      var then = obj.then;
    } catch (err) {
      // 对应 Promise A+ 规范 2.3.3.2
      return reject(promise, err);
    }

    // 如果 then 是函数,将 value 作为函数的作用域 this 调用之
    // 对应 Promise A+ 规范 2.3.3.3
    if (typeof then === 'function') {
      try {
        // 执行 then 函数
        then.call(value, function (value) {
          resolve(promise, value);
        }, function (reason) {
          reject(promise, reason);
        })
      } catch (err) {
        reject(promise, err);
      }
      return;
    }
  }
  
  // 改变 promise 内部状态为 `resolved`
  // 对应 Promise A+ 规范 2.3.3.4、2.3.4
  promise._state = 1;
  promise._value = value;

  // promise 存在 then 注册回调函数
  if (promise._deferreds.length !== 0) {
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    // 清空 then 注册回调处理数组
    promise._deferreds = [];
  }
}

resolveЛогика функции более сложная и в основном сосредоточена на обработкеvalue(x) оценивают несколько возможностей. еслиvalueдляPromiseи статусpendingкогдаpromiseприниматьvalueположение дел. существуетvalueСостояниеpending, Проще говоряpromiseизdeferredsДан массив обработки обратного вызоваvalue deferredsПеременная. Нетpendingсостояние, использованиеvalueОбратный вызов внутреннего значенияpromiseЗарегистрированоdeferreds.

еслиvalueдляthenableобъект, сvalueОбъем как функцияthisВызовите его, в то время как обратный вызов вызывает внутреннийresolve(..),reject(..)функция.

В других случаяхvalueвыполнить для параметровpromise,передачаonResolvedилиonRejectedфункция обработчика.

Фактически,Promise A+规范ОпределенныйPromise Resolution ProcedureТехнологический поток используется для обработкиthen(..)ЗарегистрированоonResolvedилиonRejectedвызовите возвращаемое значение сthenвновь сгенерированныйpromiseотношения между. Но учитываяfnвызов внутри функцииresolve(..)доходность и текущийpromiseЗначение по-прежнему имеет ту же связь, логику и записывает тот же модуль.

функция отклонения

Promiseвнутренний частный методrejectВ сравнении сresolveЛогика намного проще. Следующим образом:

function reject (promise, reason) {
  // 非 pending 状态不可变
  if (promise._state !== 0) return;

  // 改变 promise 内部状态为 `rejected`
  promise._state = 2;
  promise._value = reason;

  // 判断是否存在 then(..) 注册回调处理
  if (promise._deferreds.length !== 0) {
    // 异步执行回调函数
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    promise._deferreds = [];
  }
}

функция handleResolved

пониматьPromiseКонструктор,thenфункции и внутренниеresolveа такжеrejectРеализация функции, вы обнаружите, что все выполнения обратного вызова вызываются единообразноhandleResolvedфункция, чтоhandleResolvedЧто вы делали и на что следует обратить внимание?

handleResolvedФункция будет основана наpromiseВызов судебного решения текущего состоянияonResolved,onRejected,иметь дело сthen(..)Зарегистрируйте обратный вызов пустым и поддерживайте цепочкуthen(..)Последующие вызовы функции. Конкретная реализация выглядит следующим образом:

function handleResolved (promise, deferred) {
  // 异步执行注册回调
  asyncFn(function () {
    var cb = promise._state === 1 ? 
            deferred.onResolved : deferred.onRejected;

    // 传递注册回调函数为空情况
    if (cb === null) {
      if (promise._state === 1) {
        resolve(deferred.promise, promise._value);
      } else {
        reject(deferred.promise, promise._value);
      }
      return;
    }

    // 执行注册回调操作
    try {
      var res = cb(promise._value);
    } catch (err) {
      reject(deferred.promise, err);
    }

    // 处理链式 then(..) 注册处理函数调用
    resolve(deferred.promise, res);
  });
}

Функция обратного вызова регистрации специальной обработкиcbпустой, как в примере ниже. Определить текущий обратный вызовcbКогда пусто, используйтеdeferred.promiseкак текущийpromiseкомбинироватьvalueВызовите последующую функцию обработки, чтобы продолжить выполнение, и поймите, что значение передается через пустую функцию обработки.

Promise.resolve(233)
  .then()
  .then(function (value) {
    console.log(value)
  })

оthenЦепные вызовы, давайте кратко поговорим об этом. выполнитьthenСцепленные вызовы функций, просто должны быть вPromise.prototype.then(..)вернуть новый в функции-обработчикеpromiseПросто пример. Но помимо этого необходимо вызватьthenЗарегистрированный обработчик обратного вызова. Такие какhandleResolvedПоследнее предложение функцииresolve(deferred.promise, res)показано.

Почему зарегистрированная функция обратного вызова затем выполняется асинхронно

Вот ответ на вопрос, заданный в начале,thenЗарегистрированоonResolved,onRejectedПочему функции должны выполняться асинхронно? Давайте посмотрим на пример кода.

var a = 1;

promise1.then(function (value) {
  a = 2;
})

console.log(a)

Неизвестно, выполняет ли promise1 синхронную или асинхронную операцию внутри себя. если не указаноthenЕсли обратный вызов зарегистрирован для асинхронного выполнения, здесь может быть два значения для вывода a. a === 2, когда promise1 работает синхронно внутри, и a === 1 при выполнении асинхронных операций наоборот. Чтобы замаскировать неопределенность зависимости от внешних факторов, в спецификации указываетсяonFulfilledа такжеonRejectedСпособ выполняется асинхронно.

обещание внутренней ошибки или исключения

еслиpromiseодеялоrejected, обратный вызов отклонения вызывается с переданной причиной отклонения. например, вPromiseпри создании(fnКогда возникает исключение, оно будет перехвачено и вызваноonRejected.

Но есть еще деталь, еслиPromiseпозвони, когда закончишьonResolvedЧто делать, если при просмотре результатов возникает ошибка исключения? Обратите внимание, что в это времяonRejectedне будет запускаться для выполнения, потому чтоonResolvedВнутреннее исключение не изменяет текущийpromiseстатус (все ещеresolved), но изменитьthenвернуть новый вpromiseСтатусrejected. Исключение не теряется, но обработчик ошибок не вызывается.

Как с этим бороться?EcmascriptСпецификации определеныPromise.prototype.catchметод, если вы правыonResolvedНет уверенности в процессе или есть нештатный случай, лучшеthenвызов после функцииcatchМетод ловит исключение и обрабатывает итоговую строку.

Реализация метода, связанного с обещанием

чек об оплатеPromiseСвязанные документы или книги, вы также найдетеPromiseСвязанные полезные API:Promise.race,Promise.all,Promise.resolve,Promise.reject. Прямо здесьPromise.raceПоказана реализация метода, а остальное можно реализовать самостоятельно.

Promise.race = function (values) {
  return new Promise(function (resolve, reject) {
    values.forEach(function(value) {
      Promise.resolve(value).then(resolve, reject);
    });
  });
};

Эпилог

Написано здесь, ядроPromiseРеализация также постепенно завершается,PromiseВнутренние детали также описаны в тексте или в коде. Ввиду ограниченных возможностей автора, дляpromiseВнутренняя реализация еще не достигла уровня Paoding Jie Niu, а некоторые места были упомянуты одним махом, что может вызвать сомнения в сердцах читателей. Для неясных мест рекомендуется прочитать его дважды или обратиться к книгам, чтобы понять.

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

приложение

Справочная документация

  1. ECMA262 Promise

  2. Promises/A+ Specification

  3. [Перевод] Обещания/Спецификация A+

  4. then/promise

  5. Напишите обещание, которое соответствует спецификации Promises/A+ и работает с async/await ES7.

  6. Проанализируйте внутреннюю структуру Promise, шаг за шагом, чтобы реализовать полный класс Promise, который может пройти все тестовые случаи.

  7. Анализ основ Promise

Справочная литература

  1. «JavaScript, которого вы не знаете (Том 2)»

  2. "Углубленное понимание ES6"

  3. Дизайн JavaScript Framework (2-е издание)

  4. Введение в стандарт ES6 (3-е издание)

event loop

  1. Tasks, microtasks, queues and schedules

  2. [Перевод] Задачи, микрозадачи, очереди и расписания

  3. Difference between microtask and macrotask within an event loop context

  4. Изучите время асинхронного рендеринга javaScript и обновления браузера из спецификации цикла событий.

  5. Глубокое погружение в проблемы синхронизации с циклом событий и рендерингом в браузере

Реклама, прошу обратить внимание на публичный номер автора