Как реализована библиотека обещаний / отсрочек?
Как реализована библиотека обещания / отсрочки типа q? Я пытался прочитать исходный код, но понял, что его довольно сложно понять, поэтому я подумал, что было бы здорово, если бы кто-нибудь мог бы объяснить мне с высокого уровня, какие методы используются для реализации обещаний в однопоточных средах JS как Node и браузеры.
5 ответов
Мне сложнее объяснить, чем показать пример, поэтому здесь очень простая реализация того, что может быть отсрочкой / обещанием.
Отказ от ответственности: это не функциональная реализация, и некоторые части спецификации Promise/A отсутствуют, это просто для объяснения основы обещаний.
tl; dr: перейдите в раздел Создание классов и пример, чтобы увидеть полную реализацию.
Promise:
Сначала нам нужно создать объект обещания с массивом обратных вызовов. Я начну работать с объектами, потому что это понятнее:
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);
});
}
};
Обратите внимание, что обратные вызовы вызываются по таймауту, чтобы код всегда был асинхронным.
И это то, что нужно для реализации базовых отсрочек / обещаний.
Создайте классы и пример:
Теперь давайте преобразуем оба объекта в классы, сначала обещание:
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, обещание узла, ...) соответствуют спецификации Promises / A.
Надеюсь, я был достаточно ясен.
Редактировать:
Как и просили в комментариях, я добавил две вещи в этой версии:
- Возможность вызвать обещание, независимо от того, какой у него статус.
- Возможность связать обещания.
Чтобы иметь возможность вызвать обещание после его разрешения, вам нужно добавить статус в обещание, а когда он вызывается, проверить этот статус. Если статус разрешен или отклонен, просто выполните обратный вызов с его данными или ошибкой.
Чтобы иметь возможность связывать обещания, вы должны генерировать новую отсрочку для каждого вызова 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);
})
}
};
Как видите, он немного вырос.
Q - очень сложная библиотека обещаний с точки зрения реализации, поскольку она нацелена на поддержку конвейерной передачи и сценариев типов RPC. У меня здесь есть своя собственная реализация спецификации Promises/A+.
В принципе все довольно просто. До того как обещание выполнено / разрешено, вы ведете запись любых обратных вызовов или ошибок, помещая их в массив. Когда обещание выполнено, вы вызываете соответствующие обратные вызовы или ошибки и записываете, с каким результатом обещание было выполнено (и было ли оно выполнено или отклонено). После того, как все решено, вы просто вызываете обратные вызовы или ошибки с сохраненным результатом.
Это дает вам приблизительно семантику done
, Строить then
вам просто нужно вернуть новое обещание, которое разрешается в результате вызова callbacks/errbacks.
Если вы заинтересованы в полном объяснении причин, лежащих в основе разработки полной реализации обещаний с поддержкой RPC и конвейеризацией, такой как Q, вы можете прочитать аргументацию kriskowal здесь. Это действительно хороший градуированный подход, который я не могу рекомендовать достаточно высоко, если вы думаете о выполнении обещаний. Это, вероятно, стоит прочитать, даже если вы просто собираетесь использовать библиотеку обещаний.
Как упоминает в своем ответе Forbes, я описал многие дизайнерские решения, связанные с созданием такой библиотеки, как Q, здесь https://github.com/kriskowal/q/tree/v1/design. Достаточно сказать, что есть уровни библиотеки обещаний и множество библиотек, которые останавливаются на разных уровнях.
На первом уровне, определенном спецификацией Promises/A+, обещание является прокси для конечного результата и подходит для управления "локальной асинхронностью". То есть он подходит для обеспечения того, чтобы работа выполнялась в правильном порядке, а также для обеспечения того, чтобы было просто и понятно слушать результат операции независимо от того, была ли она уже завершена или произойдет в будущем. Это также позволяет одной или нескольким сторонам подписаться на конечный результат.
Q, как я это реализовал, предоставляет обещания, которые являются прокси для возможных, удаленных или возможных + удаленных результатов. С этой целью его дизайн перевернут, с различными реализациями для обещаний - отложенные обещания, выполненные обещания, отклоненные обещания и обещания для удаленных объектов (последнее реализовано в Q-Connection). Все они используют один и тот же интерфейс и работают, отправляя и получая сообщения типа "затем" (что достаточно для Promises/A+), а также "получить" и "вызвать". Таким образом, Q относится к "распределенной асинхронности" и существует на другом уровне.
Тем не менее, Q был фактически снят с более высокого уровня, где обещания используются для управления распределенной асинхронностью среди взаимно подозрительных сторон, таких как вы, продавец, банк, Facebook, правительство - не враги, возможно, даже друзья, но иногда с конфликтами интерес. Вопрос, который я реализовал, разработан, чтобы быть API-совместимым с усиленными обещаниями безопасности (что является причиной разделения promise
а также resolve
), с надеждой, что он познакомит людей с обещаниями, обучит их использованию этого API и позволит им брать с собой свой код, если им потребуется использовать обещания в безопасных гибридных приложениях в будущем.
Конечно, при перемещении вверх по слоям есть компромиссы, обычно по скорости. Таким образом, реализации обещаний также могут быть разработаны, чтобы сосуществовать. Вот где входит понятие "жизнеспособного". Библиотеки Promise на каждом уровне могут быть спроектированы так, чтобы использовать обещания от любого другого уровня, поэтому несколько реализаций могут сосуществовать, и пользователи могут покупать только то, что им нужно.
Все это говорит, что нет никаких оснований для того, чтобы быть трудным для чтения. Domenic и я работаем над версией 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, он будет ассимилирован вместо того, чтобы быть вложенным.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; };
Дальнейшие концепции будут поддерживать отдельное состояние ошибки (с дополнительным обратным вызовом для него) и перехватывать исключения в обработчиках, или гарантировать асинхронность для обратных вызовов. Как только вы добавите их, вы получите полностью функциональную реализацию 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;
};
Возможно, вы захотите проверить сообщение в блоге на Adehun.
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 принимает два необязательных аргумента (обработчики 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, executeFallBack = function (value) {return value; }, rejectFallBack = function (reason) {throw reason; };
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); } });
}
Функция Resolve - Разрешение обещаний
Это, вероятно, самая важная часть реализации обещания, поскольку она обрабатывает разрешение обещания. Он принимает два параметра - обещание и значение его разрешения.
Пока есть много проверок для различных возможных значений разрешения; Интересны два сценария разрешения: сценарий, в котором передается обещание, и затем отображается (объект со значением then).
- Передача значения Promise
Если значением разрешения является другое обещание, тогда обещание должно принять состояние этого значения разрешения. Поскольку это значение разрешения может быть отложено или установлено, самый простой способ сделать это - присоединить новый обработчик затем к значению разрешения и обработать исходное обещание в нем. Всякий раз, когда он улаживается, первоначальное обещание будет разрешено или отклонено.
- Передача в значимом значении
Подвох здесь в том, что функция then из значения thenable должна вызываться только один раз (хорошее применение для одноразовой оболочки из функционального программирования). Аналогичным образом, если при извлечении функции 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);
}
}
Конструктор Promise
И это тот, который объединяет все это. Функции execute и reject являются синтаксическим сахаром, которые передают функции no-op для разрешения и отклонения.
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);
});
}
};
Я надеюсь, что это помогло пролить больше света на работу с обещаниями.