Una dintre cele mai importante întrebări cu care m-am confruntat în interviuri a fost modul în care sunt puse în aplicare promisiunile. Deoarece async / await devine din ce în ce mai popular, trebuie să înțelegeți promisiunile.

Ce este o promisiune?

O promisiune este un obiect care reprezintă rezultatul unei operații asincrone care este fie rezolvată, fie respinsă (cu un motiv).

Există 3 stări

  • Îndeplinit: onFulfilled() va fi chemat (de exemplu, resolve() a fost chemat)
  • Respins: onRejected() va fi chemat (de exemplu, reject() a fost chemat)
  • In asteptarea: încă neîmplinit sau respins

Deci, să vedem cum este implementat:

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

Conform definiției de la Mozilla: Este nevoie de un executor testamentar funcționează ca 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);
}

Pare o funcție simplă cu unele proprietăți inițializate la 0 sau null. Iată câteva lucruri de observat:

this._state proprietatea poate avea trei valori posibile așa cum este descris mai sus:

0 - pending

1 - fulfilled with _value

2 - rejected with _value

3 - adopted the state of another promise, _value

Valoarea sa este0 (in asteptarea) când creați un nou promisiune.

Mai tarziu doResolve(executor, this) este invocat cu executor and promise obiect.

Să trecem la definiția lui doResolve și vedeți cum este implementat.

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

Aici sună din nou tryCallTwo funcție cu executor și 2 apeluri de apel. Apelurile din nou sună resolve și reject

done variabila este utilizată aici pentru a vă asigura că promisiunea este rezolvată sau respinsă o singură dată, deci dacă încercați să respingeți sau să rezolvați o promisiune de mai multe ori, atunci aceasta va reveni deoarece done = true.

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

Această funcție apelează indirect principalul executor callback cu 2 argumente. Aceste argumente conțin logică despre cum resolve sau reject ar trebui chemat. Puteți verifica resolveCallback și respinge apelare în doResolve funcția de mai sus.

Dacă există o eroare în timpul execuției, aceasta va stoca eroarea în LAST_ERROR și returnează eroarea.

Înainte de a sări la resolve definiția funcției, să verificăm .then funcția mai întâi:

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

Deci, în funcția de mai sus, atunci se creează noi promise și atribuirea acesteia ca proprietate unei noi funcții numite Handler. Handler funcția are argumente onCompletat și onRefuzat. Mai târziu va folosi această promisiune pentru a rezolva sau respinge cu valoare / motiv.

După cum puteți vedea, .then funcția apelează din nou o altă funcție:

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

Implementare:

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);
}
  • Există o buclă de timp care va continua să atribuie obiectul promisiunii rezolvate promisiunii curente, care este, de asemenea, o promisiune pentru _state === 3
  • Dacă _state = 0(pending) și starea promisiunii a fost amânată până când o altă promisiune imbricată este rezolvată, apelul de apel este stocat în self._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);
    }
  });
}

Ce se întâmplă:

  • Dacă statul este 1(fulfilled) apoi sunați la rezolva altceva respinge
  • Dacă onFulfilled sau onRejected este null sau dacă am folosit un gol .then() rezolvat sau respinge se va numi respectiv
  • Dacă cb nu este gol, atunci apelează o altă funcție tryCallOne(cb, self._value)
function tryCallOne(fn, a) {
   try {
     return fn(a);
   } catch (ex) {
      LAST_ERROR = ex;
     return IS_ERROR;
   }
} a) {

tryCallOne : Această funcție apelează numai apelul invers care este trecut în argument self._value. Dacă nu există nicio eroare, aceasta va rezolva promisiunea, altfel o va respinge.

Fiecare promisiune trebuie să furnizeze o .then() metoda cu următoarea semnătură:

promise.then(
  onFulfilled?: Function,
  onRejected?: Function
) => Promise
  • Ambii onFulfilled() și onRejected() sunt opționale.
  • Dacă argumentele furnizate nu sunt funcții, acestea trebuie ignorate.
  • onFulfilled() va fi chemat după ce promisiunea este îndeplinită, cu valoarea promisiunii ca prim argument.
  • onRejected() va fi chemat după ce promisiunea este respinsă, motivul respingerii fiind primul argument.
  • Nici onFulfilled() nici onRejected() poate fi apelat de mai multe ori.
  • .then() poate fi chemat de multe ori pe aceeași promisiune. Cu alte cuvinte, o promisiune poate fi utilizată pentru a agrega apelurile de apel.
  • .then() trebuie să returneze o nouă promisiune.

Promisiunea înlănțuirii

.then ar trebui să returneze o promisiune. De aceea putem crea un lanț de promisiuni de genul acesta:

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

Rezolvarea unei promisiuni

Să vedem resolve definiția funcției pe care am lăsat-o mai devreme înainte de a trece la .then():

function resolve(self, newValue) {
// Promise Resolution Procedure: https://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);
}
  • Verificăm dacă rezultatul este sau nu o promisiune. Dacă este o funcție, atunci apelați acea funcție cu valoare folosind doResolve().
  • Dacă rezultatul este o promisiune, atunci acesta va fi împins spre deferreds matrice. Puteți găsi această logică în finale funcţie.

Respingerea unei promisiuni:

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

Funcția de mai sus poate fi găsită în ./es6-extensions.js.

Ori de câte ori respingem o promisiune, .catch callback este numit, care este un strat de zahăr pentru then(null, onRejected).

Iată diagrama brută de bază pe care am creat-o, care este o vedere de păsări a ceea ce se întâmplă în interior:

Cum promite JavaScript functioneaza de fapt din interior spre

Să vedem încă o dată cum funcționează totul:

De exemplu, avem această promisiune:

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. Promisiune constructor se apelează și se creează o instanță cu new Promise
  2. executor funcția este transmisă la doResolve(executor, this) și callback unde am definit setTimeout va fi chemat de tryCallTwo(executor, resolveCallback, rejectCallback)deci va dura 3 secunde pentru a termina
  3. Sunăm .then() peste instanța promisiunii așa în fața noastră timeout este finalizat sau orice asincronizare api se intoarce, Promise.prototype.then va fi numit ca .then(cb, null)
  4. .then creează un nou promise și îl transmite ca argument către new Handler(onFulfilled, onRejected, promise)
  5. handle funcția se numește cu originalul promise instanță și handler exemplu pe care l-am creat la punctul 4.
  6. În interiorul handle funcție, curent self._state = 0 și self._deferredState = 0 asa de self_deferredState va deveni 1 și handler instanța va fi atribuită self.deferreds după acel control se va întoarce de acolo
  7. După .then() noi sunăm .catch() care va apela intern .then(null, errorCallback) – din nou se repetă aceiași pași de la punctul 4 până la punctul 6 și săriți peste punctul 7 de când am sunat .catch o singura data
  8. Actual promise starea este in asteptarea și va aștepta până când va fi rezolvat sau respins. Deci, în acest exemplu, după 3 secunde, setTimeout se apelează callback și rezolvăm în mod explicit acest lucru, care va apela resolve(value).
  9. resolveCallback va fi apelat cu valoare Time is out 🙂 și va apela principalul resolve funcție care va verifica dacă value !== null && value == 'object' && value === 'function'
  10. Va eșua în cazul nostru de când am trecut string și self._state va deveni 1 cu self._value="Time is out" Și mai târziu finale(self) se numește.
  11. finale va apela handle(self, self.deferreds) o dată pentru că self._deferredState = 1, și pentru lanțul promisiunilor, va apela handle() pentru fiecare deferred funcţie.
  12. În handle funcție, din moment ce promise este deja rezolvat, va apela handleResolved(self, deferred)
  13. handleResolved funcția va verifica dacă _state === 1 și atribuiți cb = deferred.onFulfilled care este a noastră then sună din nou. Mai tarziu tryCallOne(cb, self._value) vom apela acel apel invers și vom obține rezultatul final. În timp ce faceți acest lucru, atunci când a apărut o eroare promise va fi respins.

Când o promisiune este respinsă

În acest caz, toți pașii vor rămâne aceiași – dar în punctul 8 noi sunam reject(reason). Acest lucru va suna indirect rejectCallback definit în doResolve() și self._state va deveni 2. În finale funcţie cb va fi egal cu deferred.onRejected care va fi numit mai târziu de tryCallOne. Așa este .catch va fi apelat callback.

Asta este tot pentru acum! Sper că v-a plăcut articolul și vă va ajuta în următorul dvs. interviu JavaScript.

Dacă întâmpinați vreo problemă, nu ezitați Intrați în legătură sau comentează mai jos. Aș fi fericit să vă ajut?

Nu ezitați să bateți din palme dacă ați considerat că este o lectură utilă!

Publicat inițial la 101node.io pe 05 februarie 2019.