Una de las preguntas más importantes que enfrenté en las entrevistas fue cómo se implementan las promesas. Dado que async / await se está volviendo más popular, debe comprender las promesas.
¿Qué es una promesa?
Una promesa es un objeto que representa el resultado de una operación asincrónica que se resuelve o se rechaza (con una razón).
Hay 3 estados
- Cumplido:
onFulfilled()
será llamado (por ejemplo,resolve()
fue llamado) - Rechazado:
onRejected()
será llamado (por ejemplo,reject()
fue llamado) - Pendiente: aún no cumplido o rechazado
Entonces veamos cómo se implementa:
//github.com/then/promise/blob/master/src/core.js
Según la definición de Mozilla: toma una función ejecutora como argumento.
function noop() {} function Promise(executor) { if (typeof this !== 'object') { throw new TypeError('Promises must be constructed via new'); } if (typeof executor !== 'function') { throw new TypeError('Promise constructor\'s argument is not a function'); } this._deferredState = 0; this._state = 0; this._value = null; this._deferreds = null; if (executor === noop) return; doResolve(executor, this); }
Parece una función simple con algunas propiedades inicializadas en 0
o null
. Aquí hay algunas cosas a tener en cuenta:
this._state
La propiedad puede tener tres valores posibles como se describe arriba:
0 - pending 1 - fulfilled with _value 2 - rejected with _value 3 - adopted the state of another promise, _value
Su valor es 0
( pendiente) cuando crea una nueva promesa.
Más tarde doResolve(executor, this)
se invoca con executor and promise
object.
Pasemos a la definición de doResolve
y veamos cómo se implementa.
/** * Take a potentially misbehaving resolver function and make sure * onFulfilled and onRejected are only called once. * * Makes no guarantees about asynchrony. */ function doResolve(fn, promise) { var done = false; var resolveCallback = function(value) { if (done) return; done = true; resolve(promise, value); }; var rejectCallback = function(reason) { if (done) return; done = true; reject(promise, reason); }; var res = tryCallTwo(fn, resolveCallback, rejectCallback); if (!done && res === IS_ERROR) { done = true; reject(promise, LAST_ERROR); } }
Aquí está nuevamente llamando a la tryCallTwo
función con ejecutor y 2 devoluciones de llamada. Las devoluciones de llamada vuelven a llamar resolve
yreject
La done
variable se usa aquí para asegurarse de que la promesa se resuelva o rechace solo una vez, por lo que si intenta rechazar o resolver una promesa más de una vez, regresará porque done = true
.
function tryCallTwo(fn, a, b) { try { fn(a, b); } catch (ex) { LAST_ERROR = ex; return IS_ERROR; } }
Esta función llama indirectamente a la executor
devolución de llamada principal con 2 argumentos. Estos argumentos contienen lógica sobre cómo resolve
o reject
debería llamarse. Puede marcar resolveCallback y acceptCallback en la doResolve
función anterior .
Si hay un error durante la ejecución, almacenará el error LAST_ERROR
y lo devolverá.
Antes de saltar a la resolve
definición de la .then
función, primero revisemos la función:
Promise.prototype.then = function(onFulfilled, onRejected) { if (this.constructor !== Promise) { return safeThen(this, onFulfilled, onRejected); } var res = new Promise(noop); handle(this, new Handler(onFulfilled, onRejected, res)); return res; }; function Handler(onFulfilled, onRejected, promise) { this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null; this.onRejected = typeof onRejected === "function" ? onRejected : null; this.promise = promise; }
Entonces, en la función anterior, entonces está creando una nueva promise
y asignándola como una propiedad a una nueva función llamada Handler
. La Handler
función tiene argumentos sobre cumplido y sobre rechazado. Posteriormente utilizará esta promesa para resolver o rechazar con valor / razón.
Como puede ver, la .then
función vuelve a llamar a otra función:
handle(this, new Handler(onFulfilled, onRejected, res));
Implementación:
function handle(self, deferred) { while (self._state === 3) { self = self._value; } if (Promise._onHandle) { Promise._onHandle(self); } if (self._state === 0) { if (self._deferredState === 0) { self._deferredState = 1; self._deferreds = deferred; return; } if (self._deferredState === 1) { self._deferredState = 2; self._deferreds = [self._deferreds, deferred]; return; } self._deferreds.push(deferred); return; } handleResolved(self, deferred); }
- Hay un ciclo while que seguirá asignando el objeto de promesa resuelto a la promesa actual, que también es una promesa para
_state === 3
- Si un
_state = 0(pending)
estado de promesa se ha aplazado hasta que se resuelva otra promesa anidada, su devolución de llamada se almacena enself._deferreds
function handleResolved(self, deferred) { asap(function() { // asap is external lib used to execute cb immediately var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; if (cb === null) { if (self._state === 1) { resolve(deferred.promise, self._value); } else { reject(deferred.promise, self._value); } return; } var ret = tryCallOne(cb, self._value); if (ret === IS_ERROR) { reject(deferred.promise, LAST_ERROR); } else { resolve(deferred.promise, ret); } }); }
Qué esta pasando:
- Si el estado es 1
(fulfilled)
, llame a la resolución de lo contrario, rechace - Si
onFulfilled
oonRejected
esnull
o si usamos un vacío.then()
resuelto o rechazar se llamará respectivamente - Si
cb
no está vacío, está llamando a otra función.tryCallOne(cb, self._value)
function tryCallOne(fn, a) { try { return fn(a); } catch (ex) { LAST_ERROR = ex; return IS_ERROR; } } a) {
tryCallOne
: Esta función solo llama a la devolución de llamada que se pasa al argumento self._value
. Si no hay ningún error resolverá la promesa, de lo contrario la rechazará.
Cada promesa debe proporcionar un .then()
método con la siguiente firma:
promise.then( onFulfilled?: Function, onRejected?: Function ) => Promise
- Ambos
onFulfilled()
yonRejected()
son opcionales. - Si los argumentos proporcionados no son funciones, deben ignorarse.
onFulfilled()
se llamará después de que se cumpla la promesa, con el valor de la promesa como primer argumento.onRejected()
se llamará después de que se rechace la promesa, con el motivo del rechazo como primer argumento.- Ni
onFulfilled()
tampocoonRejected()
pueden ser convocados más de una vez. .then()
puede ser llamado muchas veces con la misma promesa. En otras palabras, una promesa se puede utilizar para agregar devoluciones de llamada..then()
debe devolver una nueva promesa.
Encadenamiento de promesas
.then
debería devolver una promesa. Es por eso que podemos crear una cadena de promesas como esta:
Promise .then(() => Promise.then(() => Promise.then(result => result) )).catch(err)
Resolviendo una promesa
Veamos la resolve
definición de función que dejamos antes antes de pasar a .then()
:
function resolve(self, newValue) { // Promise Resolution Procedure: //github.com/promises-aplus/promises-spec#the-promise-resolution-procedure if (newValue === self) { return reject( self, new TypeError("A promise cannot be resolved with itself.") ); } if ( newValue && (typeof newValue === "object" || typeof newValue === "function") ) { var then = getThen(newValue); if (then === IS_ERROR) { return reject(self, LAST_ERROR); } if (then === self.then && newValue instanceof Promise) { self._state = 3; self._value = newValue; finale(self); return; } else if (typeof then === "function") { doResolve(then.bind(newValue), self); return; } } self._state = 1; self._value = newValue; finale(self); }
- Comprobamos si el resultado es una promesa o no. Si es una función, llame a esa función con valor usando
doResolve()
. - Si el resultado es una promesa, se enviará a la
deferreds
matriz. Puedes encontrar esta lógica en lafinale
función.
Rechazar una promesa:
Promise.prototype['catch'] = function (onRejected) { return this.then(null, onRejected); };
La función anterior se puede encontrar en ./es6-extensions.js
.
Siempre que rechazamos una promesa, .catch
se llama a la devolución de llamada, que es una capa de azúcar then(null, onRejected)
.
Aquí está el diagrama aproximado básico que he creado, que es una vista de pájaro de lo que está sucediendo adentro:

Veamos una vez más cómo está funcionando todo:
For example, we have this promise:
new Promise((resolve, reject) => { setTimeout(() => { resolve("Time is out"); }, 3000) }) .then(console.log.bind(null, 'Promise is fulfilled')) .catch(console.error.bind(null, 'Something bad happened: '))
- Promise
constructor
is called and an instance is created withnew Promise
executor
function is passed todoResolve(executor, this)
and callback where we have definedsetTimeout
will be called bytryCallTwo(executor, resolveCallback, rejectCallback)
so it will take 3 seconds to finish- We are calling
.then()
over the promise instance so before ourtimeout
is completed or any asyncapi
returns,Promise.prototype.then
will be called as.then(cb, null)
.then
creates a newpromise
and passes it as an argument tonew Handler(onFulfilled, onRejected, promise)
handle
function is called with the originalpromise
instance and thehandler
instance we created in point 4.- Inside the
handle
function, currentself._state = 0
andself._deferredState = 0
soself_deferredState
will become1
andhandler
instance will be assigned toself.deferreds
after that control will return from there - After
.then()
we are calling.catch()
which will internally call.then(null, errorCallback)
— again the same steps are repeated from point 4 to point 6 and skip point 7 since we called.catch
once - Current
promise
state is pending and it will wait until it is resolved or rejected. So in this example, after 3 seconds,setTimeout
callback is called and we are resolving this explicitly which will callresolve(value)
. resolveCallback
will be called with valueTime is out
:) and it will call the mainresolve
function which will check ifvalue !== null && value == 'object' && value === 'function'
- It will fail in our case since we passed
string
andself._state
will become1
withself._value = 'Time is out'
and laterfinale(self)
is called. finale
will callhandle(self, self.deferreds)
once becauseself._deferredState = 1
, and for the chain of promises, it will callhandle()
for eachdeferred
function.- In the
handle
function, sincepromise
is resolved already, it will callhandleResolved(self, deferred)
handleResolved
function will check if_state === 1
and assigncb = deferred.onFulfilled
which is ourthen
callback. LatertryCallOne(cb, self._value)
will call that callback and we get the final result. While doing this if any error occurred thenpromise
will be rejected.
When a promise is rejected
In this case, all the steps will remain the same — but in point 8 we call reject(reason)
. This will indirectly call rejectCallback
defined in doResolve()
and self._state
will become 2
. In the finale
function cb
will be equal to deferred.onRejected
which will be called later by tryCallOne
. That’s how the .catch
callback will be called.
That's all for now! I hope you enjoyed the article and it helps in your next JavaScript interview.
If you encounter any problem feel free to get in touch or comment below. I would be happy to help ?
Don’t hesitate to clap if you considered this a worthwhile read!
Originally published at 101node.io on February 05, 2019.