javascript - tutorial - ¿Cómo se implementa una biblioteca de promesa/aplazamiento?
then javascript (5)
Como menciona Forbes en su respuesta, hice una crónica de muchas de las decisiones de diseño relacionadas con la creación de una biblioteca como Q, aquí https://github.com/kriskowal/q/tree/v1/design . Baste decir que hay niveles de una biblioteca de promesas y muchas bibliotecas que se detienen en varios niveles.
En el primer nivel, capturado por la especificación Promises / A +, una promesa es un proxy para un resultado final y es adecuada para administrar la "asincronía local" . Es decir, es adecuado para garantizar que el trabajo se realice en el orden correcto, y para garantizar que sea simple y directo escuchar el resultado de una operación, independientemente de si ya se resolvió o si ocurrirá en el futuro. También hace que sea tan simple para una o muchas partes suscribirse a un resultado final.
P, como lo he implementado, proporciona promesas que son proxies para resultados eventuales, remotos o eventuales + remotos. Para ello, su diseño se invierte, con diferentes implementaciones para promesas: promesas diferidas, promesas cumplidas, promesas rechazadas y promesas para objetos remotos (la última se implementa en Q-Connection). Todos comparten la misma interfaz y funcionan enviando y recibiendo mensajes como "luego" (que es suficiente para Promises / A +) pero también "get" e "invoke". Entonces, Q se trata de "asincronía distribuida" y existe en otra capa.
Sin embargo, la Q se eliminó de una capa superior, donde las promesas se utilizan para administrar la asincronía distribuida entre partes sospechosas como tú, un comerciante, un banco, Facebook, el gobierno, no enemigos, tal vez incluso amigos, pero a veces con conflictos de interesar. La Q que implementé está diseñada para ser compatible con API con promesas de seguridad reforzadas (que es la razón para separar la promise
y la resolve
), con la esperanza de que presente a las personas las promesas, capacítelas en el uso de esta API, y permita que tomen su código con ellos si necesitan usar promesas en mashups seguros en el futuro.
Por supuesto, hay compensaciones a medida que avanzas por las capas, generalmente en velocidad. Entonces, las implementaciones de promesas también pueden diseñarse para coexistir. Aquí es donde entra el concepto de "comerciable" . Las bibliotecas Promesa en cada capa se pueden diseñar para consumir promesas de cualquier otra capa, por lo que pueden coexistir múltiples implementaciones y los usuarios pueden comprar solo lo que necesitan.
Dicho todo esto, no hay excusa para ser difícil de leer. Domenic y yo estamos trabajando en una versión de Q que será más modular y accesible, con algunas de sus dependencias y soluciones de distracción trasladadas a otros módulos y paquetes. Afortunadamente gente como here , Crockford y otros han llenado la brecha educativa haciendo bibliotecas más simples.
¿Cómo se implementa una biblioteca de promesa / aplazamiento como q ? Intenté leer el código fuente, pero me resultó bastante difícil de entender, así que pensé que sería genial si alguien pudiera explicarme, desde un alto nivel, cuáles son las técnicas utilizadas para implementar las promesas en entornos de JS de un solo hilo. como Node y navegadores.
Es posible que desee ver la publicación del blog en Adehun.
Adehun es una implementación extremadamente ligera (aproximadamente 166 LOC) y muy útil para aprender a implementar la especificación Promise / A +.
Descargo de responsabilidad : escribí la publicación del blog, pero la publicación del blog explica todo sobre Adehun.
La función de transición: guardián de puerta para la transición del estado
Función Gatekeeper; asegura que las transiciones de estado ocurran cuando se cumplen todas las condiciones requeridas.
Si se cumplen las condiciones, esta función actualiza el estado y el valor de la promesa. A continuación, desencadena la función de proceso para su posterior procesamiento.
La función de proceso lleva a cabo la acción correcta en función de la transición (por ejemplo, pendiente de cumplimiento) y se explica más adelante.
function transition (state, value) {
if (this.state === state ||
this.state !== validStates.PENDING ||
!isValidState(state)) {
return;
}
this.value = value;
this.state = state;
this.process();
}
La función Then
La función then toma dos argumentos opcionales (manejadores de relleno y onReject) y debe devolver una nueva promesa. Dos requisitos principales:
La promesa base (aquella sobre la cual se llama) necesita crear una nueva promesa usando los manejadores pasados; la base también almacena una referencia interna a esta promesa creada, por lo que puede invocarse una vez que se cumple / rechaza la promesa base.
Si la promesa base se resuelve (es decir, se cumple o se rechaza), se debe llamar inmediatamente al controlador apropiado. Adehun.js maneja este escenario llamando proceso en la función 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;
}`
La función de proceso - Transiciones de procesamiento
Esto se llama después de las transiciones de estado o cuando se invoca la función then. Por lo tanto, es necesario verificar las promesas pendientes, ya que podría haberse invocado desde la función then.
El proceso ejecuta el procedimiento Promesa de resolución en todas las promesas almacenadas internamente (es decir, aquellas que se adjuntaron a la promesa básica a través de la función then) y aplica los siguientes requisitos Promise / A +:
Invocar a los manejadores de forma asincrónica usando el utilitario Utils.runAsync (un thin wrapper alrededor de setTimeout (setImmediate también funcionará)).
Crear controladores de respaldo para los manejadores onSuccess y onReject si faltan.
Seleccionar la función de controlador correcta en función del estado de promesa, por ejemplo, cumplido o rechazado.
Aplicando el controlador al valor de la promesa base. El valor de esta operación se pasa a la función Resolver para completar el ciclo de procesamiento prometedor.
Si se produce un error, la promesa adjunta se rechaza inmediatamente.
función process () {var that = this, fulfillFallBack = 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); } });
}
La función Resolver - Resolviendo promesas
Esta es probablemente la parte más importante de la implementación de la promesa, ya que maneja la resolución de la promesa. Acepta dos parámetros: la promesa y su valor de resolución.
Si bien hay muchos controles para varios valores posibles de resolución; los escenarios de resolución interesantes son dos: los que implican una promesa que se transfiere y un elemento recuperable (un objeto con un valor en ese momento).
- Pasar en un valor Promesa
Si el valor de resolución es otra promesa, entonces la promesa debe adoptar el estado de este valor de resolución. Dado que este valor de resolución puede estar pendiente o resuelto, la manera más fácil de hacerlo es adjuntar un nuevo manejador en ese momento al valor de resolución y manejar la promesa original en el mismo. Cuando se establece, entonces la promesa original será resuelta o rechazada.
- Pasando en un valor recuperable
El problema aquí es que la función then del valor de thenable debe invocarse solo una vez (un buen uso para el envoltorio de una vez de la programación funcional). Del mismo modo, si la recuperación de la función en ese momento arroja una excepción, la promesa debe ser rechazada inmediatamente.
Al igual que antes, la función then se invoca con funciones que finalmente resuelven o rechazan la promesa, pero la diferencia aquí es la bandera llamada que se establece en la primera llamada y las llamadas subsiguientes no son operaciones.
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);
}
}
The Promise Constructor
Y este es el que lo junta todo. Las funciones de cumplir y rechazar son azúcar sintáctico que pasan funciones no operativas para resolver y rechazar.
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);
});
}
};
Espero que esto ayude a arrojar más luz sobre la forma en que las promesas funcionan.
Me resulta más difícil de explicar que mostrar un ejemplo, así que aquí hay una implementación muy simple de lo que podría ser un aplazamiento / promesa.
Descargo de responsabilidad: Esta no es una implementación funcional y faltan algunas partes de la especificación Promesa / A. Esto es solo para explicar la base de las promesas.
tl; dr: vaya a la sección Crear clases y ejemplos para ver la implementación completa.
Promesa:
Primero, necesitamos crear un objeto de promesa con una variedad de devoluciones de llamada. Comenzaré a trabajar con objetos porque es más claro:
var promise = {
callbacks: []
}
ahora agrega devoluciones de llamada con el método, entonces:
var promise = {
callbacks: [],
then: function (callback) {
callbacks.push(callback);
}
}
Y también necesitamos las devoluciones de errores:
var promise = {
okCallbacks: [],
koCallbacks: [],
then: function (okCallback, koCallback) {
okCallbacks.push(okCallback);
if (koCallback) {
koCallbacks.push(koCallback);
}
}
}
Aplazar:
Ahora crea el objeto diferido que tendrá una promesa:
var defer = {
promise: promise
};
El aplazamiento debe ser resuelto:
var defer = {
promise: promise,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
};
Y necesita rechazar:
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);
});
}
};
Tenga en cuenta que las devoluciones de llamada se llaman en un tiempo de espera para permitir que el código siempre sea asincrónico.
Y eso es lo que necesita una implementación básica de diferimiento / promesa.
Crea clases y ejemplos:
Ahora permitamos convertir ambos objetos a clases, primero la promesa:
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);
}
}
};
Y ahora el diferido:
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);
});
}
};
Y aquí hay un ejemplo de uso:
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);
});
Como puede ver, las partes básicas son simples y pequeñas. Crecerá cuando agregue otras opciones, por ejemplo, múltiples promesas de resolución:
Defer.all(promiseA, promiseB, promiseC).then()
o prometer encadenar:
getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);
Para leer más acerca de las especificaciones: CommonJS Promise Specification . Tenga en cuenta que las bibliotecas principales (Q, when.js, rsvp.js, node-promise, ...) siguen la especificación Promises/A
Espero haber sido lo suficientemente claro.
Editar:
Como pregunté en los comentarios, agregué dos cosas en esta versión:
- La posibilidad de llamar a continuación de una promesa, sin importar el estado que tenga.
- La posibilidad de encadenar promesas.
Para poder invocar la promesa cuando se haya resuelto, debe agregar el estado a la promesa, y cuando se llame a ese estado, verifique ese estado. Si el estado es resuelto o rechazado, simplemente ejecute la devolución de llamada con sus datos o error.
Para poder encadenar promesas, debe generar un nuevo diferimiento para cada llamada y, una vez resuelta / rechazada, resolver / rechazar la nueva promesa con el resultado de la devolución de llamada. Por lo tanto, cuando la promesa finaliza, si la devolución de llamada devuelve una nueva promesa, está vinculada a la promesa devuelta con then()
. Si no, la promesa se resuelve con el resultado de la devolución de llamada.
Aquí está la promesa:
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);
}
};
Y el aplazamiento:
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);
})
}
};
Como puede ver, ha crecido bastante.
Primero asegúrate de entender cómo se supone que las promesas funcionan. Eche un vistazo a las propuestas de Promesas de CommonJs y la especificación Promises / A + para eso.
Hay dos conceptos básicos que se pueden implementar cada uno en unas pocas líneas simples:
Una promesa se resuelve de forma asíncrona con el resultado. Agregar devoluciones de llamada es una acción transparente: independientemente de si la promesa ya se ha resuelto o no, se les llamará con el resultado una vez que esté disponible.
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; }
Las promesas tienen un método que permite encadenarlas. Tomo una devolución de llamada y devuelvo una nueva promesa que se resolverá con el resultado de esa devolución de llamada después de que se invocó con el resultado de la primera promesa. Si la devolución de llamada devuelve Promesa, se asimilará en lugar de anidarse.
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; };
Otros conceptos serían mantener un estado de error por separado (con una devolución de llamada adicional para él) y capturar excepciones en los controladores, o garantizar la asincronía para las devoluciones de llamada. Una vez que los agrega, tiene una implementación Promise completamente funcional.
Aquí está el error escrito. Desafortunadamente es bastante repetitivo; puedes hacerlo mejor usando cierres adicionales, pero luego es muy difícil de entender.
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;
};
Q es una biblioteca de promesas muy compleja en términos de implementación porque tiene como objetivo apoyar el pipelling y los escenarios de tipo RPC. Tengo mi propia implementación muy básica de la especificación Promises/A+ here .
En principio, es bastante simple. Antes de que la promesa sea resuelta / resuelta, mantenga un registro de las devoluciones de llamadas o errores al insertarlas en una matriz. Cuando se solucione la promesa, debe llamar a las devoluciones de llamada o errores correspondientes y registrar el resultado con el que se resolvió la promesa (y si se cumplió o rechazó). Una vez resuelto, simplemente llama a las devoluciones de llamada o errores con el resultado almacenado.
Eso te da aproximadamente la semántica de done
. Para compilar, solo tiene que devolver una nueva promesa que se resuelve con el resultado de llamar a las devoluciones / errores.
Si está interesado en una explicación completa del razonamiento detrás del desarrollo de una implementación prometedora completa con soporte para RPC y la canalización como Q, puede leer el razonamiento de kriskowal here . Es un enfoque graduado realmente agradable que no puedo recomendar lo suficiente si estás pensando en implementar las promesas. Probablemente valga la pena leerlo incluso si va a utilizar una biblioteca de promesas.