Как реализована библиотека обещаний / отложений, такая как q? Я пытался прочитать исходный код, но мне было довольно сложно его понять, поэтому я подумал, что было бы здорово, если бы кто-нибудь мог объяснить мне на высоком уровне, какие методы используются для реализации обещаний в однопоточных средах JS. как Node и браузеры.
Как реализована библиотека обещания / отсрочки?
Ответы (5)
Мне сложнее объяснить, чем показать пример, поэтому вот очень простая реализация того, что может быть defer / prom.
Отказ от ответственности: это не функциональная реализация, и некоторые части спецификации Promise / A отсутствуют. Это просто для объяснения основы обещаний.
tl; dr: Перейдите в раздел Создание классов и примеров, чтобы увидеть полную реализацию.
Обещать:
Сначала нам нужно создать объект обещания с массивом обратных вызовов. Начну работать с объектами, потому что понятнее:
var promise = {
callbacks: []
}
теперь добавьте обратные вызовы с помощью метода, затем:
var promise = {
callbacks: [],
then: function (callback) {
callbacks.push(callback);
}
}
И нам тоже нужны обратные вызовы ошибок:
var promise = {
okCallbacks: [],
koCallbacks: [],
then: function (okCallback, koCallback) {
okCallbacks.push(okCallback);
if (koCallback) {
koCallbacks.push(koCallback);
}
}
}
Отложить:
Теперь создайте объект defer с обещанием:
var defer = {
promise: promise
};
Отсрочка должна быть разрешена:
var defer = {
promise: promise,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
};
И нужно отклонить:
var defer = {
promise: promise,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
reject: function (error) {
this.promise.koCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(error)
}, 0);
});
}
};
Обратите внимание, что обратные вызовы вызываются по таймауту, чтобы код всегда был асинхронным.
И это то, что нужно для базовой реализации defer / обещания.
Создайте классы и пример:
Теперь давайте конвертируем оба объекта в классы, сначала обещание:
var Promise = function () {
this.okCallbacks = [];
this.koCallbacks = [];
};
Promise.prototype = {
okCallbacks: null,
koCallbacks: null,
then: function (okCallback, koCallback) {
okCallbacks.push(okCallback);
if (koCallback) {
koCallbacks.push(koCallback);
}
}
};
А теперь отсрочка:
var Defer = function () {
this.promise = new Promise();
};
Defer.prototype = {
promise: null,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
reject: function (error) {
this.promise.koCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(error)
}, 0);
});
}
};
А вот пример использования:
function test() {
var defer = new Defer();
// an example of an async call
serverCall(function (request) {
if (request.status === 200) {
defer.resolve(request.responseText);
} else {
defer.reject(new Error("Status code was " + request.status));
}
});
return defer.promise;
}
test().then(function (text) {
alert(text);
}, function (error) {
alert(error.message);
});
Как видите, основные части простые и маленькие. Он будет расти, когда вы добавите другие параметры, например разрешение нескольких обещаний:
Defer.all(promiseA, promiseB, promiseC).then()
или обещание цепочки:
getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);
Чтобы узнать больше о спецификациях: CommonJS Promise Specification. Обратите внимание, что основные библиотеки (Q, when.js, rsvp.js, node-prom, ...) следуют Promises / A спецификация.
Надеюсь, я был достаточно ясен.
Редактировать:
Как и просили в комментариях, я добавил в эту версию две вещи:
- Возможность вызвать обещание, независимо от его статуса.
- Возможность связывать обещания.
Чтобы иметь возможность вызвать обещание при разрешении, вам нужно добавить статус к обещанию, а когда вызывается then, проверьте этот статус. Если статус разрешен или отклонен, просто выполните обратный вызов с его данными или ошибкой.
Чтобы иметь возможность связывать обещания, вам необходимо сгенерировать новую задержку для каждого вызова then
и, когда обещание будет разрешено / отклонено, разрешить / отклонить новое обещание с результатом обратного вызова. Итак, когда обещание выполнено, если обратный вызов возвращает новое обещание, он привязан к обещанию, возвращенному с then()
. Если нет, обещание разрешается с результатом обратного вызова.
Вот обещание:
var Promise = function () {
this.okCallbacks = [];
this.koCallbacks = [];
};
Promise.prototype = {
okCallbacks: null,
koCallbacks: null,
status: 'pending',
error: null,
then: function (okCallback, koCallback) {
var defer = new Defer();
// Add callbacks to the arrays with the defer binded to these callbacks
this.okCallbacks.push({
func: okCallback,
defer: defer
});
if (koCallback) {
this.koCallbacks.push({
func: koCallback,
defer: defer
});
}
// Check if the promise is not pending. If not call the callback
if (this.status === 'resolved') {
this.executeCallback({
func: okCallback,
defer: defer
}, this.data)
} else if(this.status === 'rejected') {
this.executeCallback({
func: koCallback,
defer: defer
}, this.error)
}
return defer.promise;
},
executeCallback: function (callbackData, result) {
window.setTimeout(function () {
var res = callbackData.func(result);
if (res instanceof Promise) {
callbackData.defer.bind(res);
} else {
callbackData.defer.resolve(res);
}
}, 0);
}
};
И отсрочка:
var Defer = function () {
this.promise = new Promise();
};
Defer.prototype = {
promise: null,
resolve: function (data) {
var promise = this.promise;
promise.data = data;
promise.status = 'resolved';
promise.okCallbacks.forEach(function(callbackData) {
promise.executeCallback(callbackData, data);
});
},
reject: function (error) {
var promise = this.promise;
promise.error = error;
promise.status = 'rejected';
promise.koCallbacks.forEach(function(callbackData) {
promise.executeCallback(callbackData, error);
});
},
// Make this promise behave like another promise:
// When the other promise is resolved/rejected this is also resolved/rejected
// with the same data
bind: function (promise) {
var that = this;
promise.then(function (res) {
that.resolve(res);
}, function (err) {
that.reject(err);
})
}
};
Как видите, он немного вырос.
then
не возвращает другое обещание (что очень важно).
- person Bergi; 18.07.2013
How is a promise/defer library like q implemented
и it'd be great if someone could explain to me, from a high level, what are the techniques used to implement promises
. Чтобы объяснить это, я показывал код постепенно, чтобы сосредоточиться на некоторых частях без потери контекста. Вы объясняете спецификацию и то, что должно быть, а не то, как она реализована, а затем вы показываете очень маленькую реализацию, которая не похожа на Q. И в частности, он просит promise/defer
реализацию, а у вас нет отсрочки.
- person Kaizo; 18.07.2013
then
ничего не возвращает, верно? Если да, то можете ли вы также показать нам, что then
должен фактически вернуть, чтобы включить цепочку? Мне не сразу стало ясно.
- person Derek Chiang; 18.07.2013
Promise
действительно используется?
- person towry; 26.02.2015
Note: This method is not expected to become standard, and is only implemented by recent builds of Internet Explorer and Node.js 0.10+. It meets resistance both from Gecko (Firefox) and Webkit (Google/Apple).
- person Kaizo; 21.05.2015
Q - очень сложная библиотека обещаний с точки зрения реализации, поскольку она нацелена на поддержку конвейерной обработки и сценариев типов RPC. У меня есть собственная очень простая реализация спецификации Promises / A + здесь.
В принципе все довольно просто. Прежде чем обещание будет урегулировано / разрешено, вы сохраняете запись любых обратных вызовов или ошибок, помещая их в массив. Когда обещание выполнено, вы вызываете соответствующие обратные вызовы или ошибки и записываете, с каким результатом было согласовано обещание (и было ли оно выполнено или отклонено). После этого вы просто вызываете обратные вызовы или ошибки с сохраненным результатом.
Это дает вам примерно семантику done
. Чтобы построить then
, вам просто нужно вернуть новое обещание, которое разрешается в результате вызова обратных вызовов / ошибок.
Если вас интересует полное объяснение причин разработки полной реализации обещаний с поддержкой RPC и конвейерной обработки, такой как Q, вы можете прочитать аргументы Крискоула здесь. Это действительно хороший поэтапный подход, который я не могу рекомендовать достаточно, если вы думаете о выполнении обещаний. Вероятно, стоит прочитать, даже если вы просто собираетесь использовать библиотеку обещаний.
Как упоминает Forbes в своем ответе, я записал многие дизайнерские решения, связанные с созданием такой библиотеки, как Q, здесь https://github.com/kriskowal/q/tree/v1/design. Достаточно сказать, что есть уровни библиотеки обещаний и множество библиотек, которые останавливаются на разных уровнях.
На первом уровне, охваченном спецификацией Promises / A +, обещание является прокси для конечного результата и подходит для управления «локальной асинхронностью». То есть он подходит для обеспечения того, чтобы работа выполнялась в правильном порядке, а также для обеспечения того, чтобы было просто и понятно отслеживать результат операции, независимо от того, был ли он уже установлен или произойдет в будущем. Это также позволяет одной или нескольким сторонам подписаться на конечный результат.
Q, как я его реализовал, предоставляет обещания, которые являются прокси для возможных, удаленных или возможных + удаленных результатов. С этой целью его дизайн инвертирован, с различными реализациями обещаний - отложенные обещания, выполненные обещания, отклоненные обещания и обещания для удаленных объектов (последнее из них реализовано в Q-Connection). Все они используют один и тот же интерфейс и работают, отправляя и получая сообщения, такие как then (что достаточно для Promises / A +), но также получают и вызывают. Итак, Q относится к распределенной асинхронности и существует на другом уровне.
Однако на самом деле Q был удален с более высокого уровня, где обещания используются для управления распределенной асинхронностью между взаимно подозрительными сторонами, такими как вы, продавец, банк, Facebook, правительство - не врагами, может даже друзья, но иногда с конфликтом интересов. Реализованный мной Q разработан так, чтобы быть совместимым с API с усиленными обещаниями безопасности (что является причиной разделения promise
и resolve
), в надежде, что он познакомит людей с обещаниями, обучит их использованию этого API и позволит им принимать их код с ними, если им нужно использовать обещания в безопасных гибридных приложениях в будущем.
Конечно, по мере продвижения по слоям есть компромиссы, обычно в скорости. Таким образом, реализации обещаний также могут быть спроектированы для сосуществования. Вот тут-то и появляется понятие «полезного». Библиотеки обещаний на каждом уровне могут быть разработаны для использования обещаний с любого другого уровня, поэтому несколько реализаций могут сосуществовать, и пользователи могут покупать только то, что им нужно.
Все это говорит о том, что нет никаких оправданий тому, что его трудно читать. Мы с Домеником работаем над версией Q, которая будет более модульной и доступной, с некоторыми отвлекающими зависимостями и обходными путями, перенесенными в другие модули и пакеты. К счастью, такие люди, как Forbes, Crockford и другие заполнили образовательный пробел, сделав более простые библиотеки.
Сначала убедитесь, что вы понимаете, как должны работать обещания. Ознакомьтесь с предложениями CommonJs Promises и Promises / спецификация A + для этого.
Есть две основные концепции, каждая из которых может быть реализована в несколько простых строк:
Обещание выполняется асинхронно с результатом. Добавление обратных вызовов - это прозрачное действие - независимо от того, выполнено ли обещание или нет, они будут вызваны с результатом, как только он станет доступен.
function Deferred() { var callbacks = [], // list of callbacks result; // the resolve arguments or undefined until they're available this.resolve = function() { if (result) return; // if already settled, abort result = arguments; // settle the result for (var c;c=callbacks.shift();) // execute stored callbacks c.apply(null, result); }); // create Promise interface with a function to add callbacks: this.promise = new Promise(function add(c) { if (result) // when results are available c.apply(null, result); // call it immediately else callbacks.push(c); // put it on the list to be executed later }); } // just an interface for inheritance function Promise(add) { this.addCallback = add; }
У обещаний есть
then
метод, который позволяет объединять их в цепочку. Я принимаю обратный вызов и возвращаю новое обещание, которое будет разрешено с результатом этого обратного вызова после того, как оно было вызвано с результатом первого обещания. Если обратный вызов возвращает обещание, оно будет ассимилировано, а не вложено.Promise.prototype.then = function(fn) { var dfd = new Deferred(); // create a new result Deferred this.addCallback(function() { // when `this` resolves… // execute the callback with the results var result = fn.apply(null, arguments); // check whether it returned a promise if (result instanceof Promise) result.addCallback(dfd.resolve); // then hook the resolution on it else dfd.resolve(result); // resolve the new promise immediately }); }); // and return the new Promise return dfd.promise; };
Дальнейшие концепции будут поддерживать отдельное состояние error (с дополнительным обратным вызовом для него) и улавливать исключения в обработчиках или гарантировать асинхронность для обратных вызовов. Как только вы их добавите, у вас будет полностью функциональная реализация Promise.
Вот выписанная ошибка. К сожалению, это довольно часто повторяется; вы можете добиться большего, используя дополнительные замыкания, но тогда это становится действительно очень трудно понять.
function Deferred() {
var callbacks = [], // list of callbacks
errbacks = [], // list of errbacks
value, // the fulfill arguments or undefined until they're available
reason; // the error arguments or undefined until they're available
this.fulfill = function() {
if (reason || value) return false; // can't change state
value = arguments; // settle the result
for (var c;c=callbacks.shift();)
c.apply(null, value);
errbacks.length = 0; // clear stored errbacks
});
this.reject = function() {
if (value || reason) return false; // can't change state
reason = arguments; // settle the errror
for (var c;c=errbacks.shift();)
c.apply(null, reason);
callbacks.length = 0; // clear stored callbacks
});
this.promise = new Promise(function add(c) {
if (reason) return; // nothing to do
if (value)
c.apply(null, value);
else
callbacks.push(c);
}, function add(c) {
if (value) return; // nothing to do
if (reason)
c.apply(null, reason);
else
errbacks.push(c);
});
}
function Promise(addC, addE) {
this.addCallback = addC;
this.addErrback = addE;
}
Promise.prototype.then = function(fn, err) {
var dfd = new Deferred();
this.addCallback(function() { // when `this` is fulfilled…
try {
var result = fn.apply(null, arguments);
if (result instanceof Promise) {
result.addCallback(dfd.fulfill);
result.addErrback(dfd.reject);
} else
dfd.fulfill(result);
} catch(e) { // when an exception was thrown
dfd.reject(e);
}
});
this.addErrback(err ? function() { // when `this` is rejected…
try {
var result = err.apply(null, arguments);
if (result instanceof Promise) {
result.addCallback(dfd.fulfill);
result.addErrback(dfd.reject);
} else
dfd.fulfill(result);
} catch(e) { // when an exception was re-thrown
dfd.reject(e);
}
} : dfd.reject); // when no `err` handler is passed then just propagate
return dfd.promise;
};
addCallback
в классе Promise
будет вызываться более одного раза? Метод then
просто вернет новый экземпляр Promise
, так зачем же хранить массив обратных вызовов в классе Deferred
?
- person towry; 26.02.2015
then
можно вызывать несколько раз для одного и того же обещания, ничто не мешает этому. Только потому, что многие цепочки обещаний линейны, это не означает, что ветвление не поддерживается.
- person Bergi; 26.02.2015
Вы можете прочитать сообщение в блоге на Адехуне.
Adehun - чрезвычайно легкая реализация (около 166 LOC), очень полезная для изучения того, как реализовать спецификацию Promise / A +.
Заявление об ограничении ответственности: я написал сообщение в блоге, но сообщение в блоге объясняет все об Adehun.
Функция перехода - привратник для перехода между состояниями
Функция привратника; гарантирует, что переходы между состояниями происходят при соблюдении всех требуемых условий.
Если условия соблюдены, эта функция обновляет состояние и значение обещания. Затем он запускает функцию процесса для дальнейшей обработки.
Функция процесса выполняет правильное действие в зависимости от перехода (например, от ожидания к выполнению), что будет объяснено позже.
function transition (state, value) {
if (this.state === state ||
this.state !== validStates.PENDING ||
!isValidState(state)) {
return;
}
this.value = value;
this.state = state;
this.process();
}
Функция Then
Функция then принимает два необязательных аргумента (обработчики onFulfill и onReject) и должна возвращать новое обещание. Два основных требования:
Базовое обещание (то, которое затем вызывается) должно создать новое обещание, используя переданные обработчики; база также хранит внутреннюю ссылку на это созданное обещание, чтобы его можно было вызвать после выполнения / отклонения базового обещания.
Если базовое обещание установлено (то есть выполнено или отклонено), то соответствующий обработчик должен быть вызван немедленно. Adehun.js обрабатывает этот сценарий, вызывая процесс в функции then.
``
function then(onFulfilled, onRejected) {
var queuedPromise = new Adehun();
if (Utils.isFunction(onFulfilled)) {
queuedPromise.handlers.fulfill = onFulfilled;
}
if (Utils.isFunction(onRejected)) {
queuedPromise.handlers.reject = onRejected;
}
this.queue.push(queuedPromise);
this.process();
return queuedPromise;
}`
Функция обработки - обработка переходов
Это вызывается после переходов между состояниями или при вызове функции then. Таким образом, ему необходимо проверить ожидающие обещания, поскольку он мог быть вызван из функции then.
Процесс запускает процедуру разрешения обещаний для всех внутренних сохраненных обещаний (то есть тех, которые были прикреплены к базовому обещанию с помощью функции then) и обеспечивает выполнение следующих требований Promise / A +:
Асинхронный вызов обработчиков с помощью помощника Utils.runAsync (тонкая оболочка вокруг setTimeout (setImmediate также будет работать)).
Создание резервных обработчиков для обработчиков onSuccess и onReject, если они отсутствуют.
Выбор правильной функции обработчика на основе состояния обещания, например. выполнено или отклонено.
Применение обработчика к значению базового обещания. Значение этой операции передается в функцию Resolve для завершения цикла обработки обещания.
При возникновении ошибки прикрепленное обещание немедленно отклоняется.
function process () {var that = this, performFallBack = function (value) {возвращаемое значение; }, rejectFallBack = функция (причина) {бросить причину; };
if (this.state === validStates.PENDING) { return; } Utils.runAsync(function() { while (that.queue.length) { var queuedP = that.queue.shift(), handler = null, value; if (that.state === validStates.FULFILLED) { handler = queuedP.handlers.fulfill || fulfillFallBack; } if (that.state === validStates.REJECTED) { handler = queuedP.handlers.reject || rejectFallBack; } try { value = handler(that.value); } catch (e) { queuedP.reject(e); continue; } Resolve(queuedP, value); } });
}
Функция выполнения - выполнение обещаний
Это, вероятно, самая важная часть реализации обещания, поскольку она обрабатывает разрешение обещания. Он принимает два параметра - обещание и значение его разрешения.
Хотя существует множество проверок различных возможных значений разрешения; Есть два интересных сценария разрешения: те, которые включают в себя переданное обещание и thenable (объект со значением then).
- Передача значения обещания
Если значением разрешения является другое обещание, то обещание должно принять состояние этого значения разрешения. Поскольку это значение разрешения может быть отложенным или установленным, самый простой способ сделать это - присоединить новый обработчик then к значению разрешения и обработать в нем исходное обещание. Как только он уляжется, исходное обещание будет выполнено или отклонено.
- Передача полезного значения
Загвоздка здесь в том, что функция then для пригодного для использования значения должна быть вызвана только один раз (хорошее применение для оболочки Once из функционального программирования). Точно так же, если получение функции then вызывает исключение, обещание должно быть немедленно отклонено.
Как и раньше, функция then вызывается с функциями, которые в конечном итоге разрешают или отклоняют обещание, но разница здесь в флаге вызова, который устанавливается при первом вызове и делает последующие вызовы не операциями.
function Resolve(promise, x) {
if (promise === x) {
var msg = "Promise can't be value";
promise.reject(new TypeError(msg));
}
else if (Utils.isPromise(x)) {
if (x.state === validStates.PENDING){
x.then(function (val) {
Resolve(promise, val);
}, function (reason) {
promise.reject(reason);
});
} else {
promise.transition(x.state, x.value);
}
}
else if (Utils.isObject(x) ||
Utils.isFunction(x)) {
var called = false,
thenHandler;
try {
thenHandler = x.then;
if (Utils.isFunction(thenHandler)){
thenHandler.call(x,
function (y) {
if (!called) {
Resolve(promise, y);
called = true;
}
}, function (r) {
if (!called) {
promise.reject(r);
called = true;
}
});
} else {
promise.fulfill(x);
called = true;
}
} catch (e) {
if (!called) {
promise.reject(e);
called = true;
}
}
}
else {
promise.fulfill(x);
}
}
Конструктор обещаний
И это то, что объединяет все воедино. Функции выполнения и отклонения являются синтаксическим сахаром, передающим неработающие функции для разрешения и отклонения.
var Adehun = function (fn) {
var that = this;
this.value = null;
this.state = validStates.PENDING;
this.queue = [];
this.handlers = {
fulfill : null,
reject : null
};
if (fn) {
fn(function (value) {
Resolve(that, value);
}, function (reason) {
that.reject(reason);
});
}
};
Я надеюсь, что это помогло пролить больше света на то, как работают обещания.