Hvordan JavaScript-løfter faktisk fungerer indefra og ud

Et af de vigtigste spørgsmål, jeg stod over for i interviews, var hvordan løfter gennemføres. Da asynkronisering / afventning bliver mere populær, skal du forstå løfter.

Hvad er et løfte?

Et løfte er et objekt, der repræsenterer resultatet af en asynkron operation, som enten løses eller afvises (med en grund).

Der er 3 stater

  • Opfyldt:onFulfilled() kaldes (fx resolve()blev kaldt)
  • Afvist:onRejected() kaldes (fx reject()blev kaldt)
  • Afventer: endnu ikke opfyldt eller afvist

Så lad os se, hvordan det implementeres:

//github.com/then/promise/blob/master/src/core.js

Ifølge definitionen i Mozilla: Det tager en eksekutorfunktion som et argument.

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); }

Ser ud som en simpel funktion med nogle egenskaber initialiseret til 0eller null. Her er et par ting at bemærke:

this._stateegenskab kan have tre mulige værdier som beskrevet ovenfor:

0 - pending 1 - fulfilled with _value 2 - rejected with _value 3 - adopted the state of another promise, _value

Dens værdi er 0( afventende), når du opretter et nyt løfte.

Senere doResolve(executor, this)påberåbes med executor and promiseobjekt.

Lad os gå videre til definitionen af doResolveog se, hvordan den implementeres.

/** * 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); } }

Her kaldes det igen til tryCallTwofunktion med eksekutor og 2 tilbagekald. Callbacks ringer igen resolveogreject

Den donevariabel bruges her for at sikre det løfte er løst eller afvist én gang, så hvis du forsøger at afvise eller løse et løfte mere end én gang, så vil det vende tilbage, fordi done = true.

function tryCallTwo(fn, a, b) { try { fn(a, b); } catch (ex) { LAST_ERROR = ex; return IS_ERROR; } }

Denne funktion kalder indirekte hovedtilbagekaldet executormed 2 argumenter. Disse argumenter indeholder logik om, hvordan resolveeller rejectskal kaldes. Du kan kontrollere resolutionCallback og afvis Callback i doResolvefunktionen ovenfor .

Hvis der er en fejl under udførelsen, gemmer den fejlen LAST_ERRORog returnerer fejlen.

Før vi springer til resolvefunktionsdefinitionen, lad os først tjekke .thenfunktionen:

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; }

Så i ovenstående funktion oprettes derefter nyt promiseog tildeles det som en egenskab til en ny funktion kaldet Handler. Den Handlerfunktion har argumenter onFulfilled og onRejected. Senere vil det bruge dette løfte til at løse eller afvise med værdi / grund.

Som du kan se, .thenkalder funktionen igen en anden funktion:

handle(this, new Handler(onFulfilled, onRejected, res));

Implementering:

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); }
  • Der er en stund-løkke, som fortsat tildeler det løste løfteobjekt til det aktuelle løfte, som også er et løfte om _state === 3
  • Hvis _state = 0(pending)tilstanden og løftet er udsat, indtil et andet nestet løfte er løst, gemmes dets tilbagekaldelse iself._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); } }); }

Hvad sker der:

  • Hvis staten er 1, (fulfilled)skal du kalde beslutningen ellers afvise
  • Hvis onFulfilledeller onRejecteder, nulleller hvis vi brugte et tomt .then()løst eller afvist , kaldes det henholdsvis
  • Hvis den cbikke er tom, kalder den en anden funktiontryCallOne(cb, self._value)
function tryCallOne(fn, a) { try { return fn(a); } catch (ex) { LAST_ERROR = ex; return IS_ERROR; } } a) {

tryCallOne: Denne funktion kalder kun det tilbagekald, der sendes ind i argumentet self._value. Hvis der ikke er nogen fejl, løser det løftet, ellers afviser det det.

Hvert løfte skal levere en .then()metode med følgende underskrift:

promise.then( onFulfilled?: Function, onRejected?: Function ) => Promise
  • Begge onFulfilled()og onRejected()er valgfri.
  • Hvis de leverede argumenter ikke er funktioner, skal de ignoreres.
  • onFulfilled() vil blive kaldt efter løftet er opfyldt, med løfteværdien som det første argument.
  • onRejected() vil blive kaldt, efter at løftet er afvist, med grunden til afvisning som det første argument.
  • Hverken onFulfilled()eller onRejected()må kaldes mere end én gang.
  • .then()kan kaldes mange gange på samme løfte. Med andre ord kan et løfte bruges til at samle tilbagekald.
  • .then() skal returnere et nyt løfte.

Lovkædning

.thenskulle returnere et løfte. Derfor kan vi skabe en kæde af løfter som denne:

Promise .then(() => Promise.then(() => Promise.then(result => result) )).catch(err)

Løfte et løfte

Lad os se resolvefunktionsdefinitionen, som vi forlod tidligere, inden vi gik videre til .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); }
  • Vi kontrollerer, om resultatet er et løfte eller ej. Hvis det er en funktion, skal du kalde den funktion med værdi ved hjælp af doResolve().
  • Hvis resultatet er et løfte, skubbes det til deferredsarrayet. Du kan finde denne logik i finalefunktionen.

Afviser et løfte:

Promise.prototype['catch'] = function (onRejected) { return this.then(null, onRejected); };

Ovenstående funktion kan findes i ./es6-extensions.js.

Når vi afviser et løfte, .catchkaldes tilbagekaldet, som er en sukkerfrakke til then(null, onRejected).

Her er det grundlæggende grove diagram, som jeg har oprettet, hvilket er et fugleperspektiv af, hvad der sker indeni:

Lad os se igen, hvordan alt fungerer:

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: '))
  1. Promise constructor is called and an instance is created with new Promise
  2. executor function is passed to doResolve(executor, this) and callback where we have defined setTimeout will be called by tryCallTwo(executor, resolveCallback, rejectCallback)so it will take 3 seconds to finish
  3. We are calling .then() over the promise instance so before our timeout is completed or any async api returns, Promise.prototype.then will be called as .then(cb, null)
  4. .then creates a new promise and passes it as an argument to new Handler(onFulfilled, onRejected, promise)
  5. handle function is called with the original promise instance and the handler instance we created in point 4.
  6. Inside the handle function, current self._state = 0 and self._deferredState = 0 so self_deferredState will become 1 and handler instance will be assigned to self.deferreds after that control will return from there
  7. 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
  8. 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 call resolve(value).
  9. resolveCallback will be called with value Time is out :) and it will call the main resolve function which will check if value !== null && value == 'object' && value === 'function'
  10. It will fail in our case since we passed string and self._state will become 1 with self._value = 'Time is out' and later finale(self) is called.
  11. finale will call handle(self, self.deferreds) once because self._deferredState = 1, and for the chain of promises, it will call handle() for each deferred function.
  12. In the handle function, since promise is resolved already, it will call handleResolved(self, deferred)
  13. handleResolved function will check if _state === 1 and assign cb = deferred.onFulfilled which is our then callback. Later tryCallOne(cb, self._value) will call that callback and we get the final result. While doing this if any error occurred then promise will be rejected.

When a promise is rejected

I dette tilfælde forbliver alle trin de samme - men i punkt 8 kalder vi reject(reason). Dette vil indirekte kalde rejectCallbackdefineret i doResolve() og self._statebliver 2. I finalefunktionen cbvil være lig deferred.onRejectedsom kaldes senere af tryCallOne. Sådan .catchkaldes tilbagekaldet.

Det er alt for nu! Jeg håber, du nød artiklen, og den hjælper i dit næste JavaScript-interview.

Hvis du støder på et problem, er du velkommen til at kontakte eller kommentere nedenfor. Jeg hjælper gerne?

Tøv ikke med at klappe, hvis du betragter dette som en god læsning!

Oprindeligt offentliggjort på 101node.io den 5. februar 2019.