Problema de JavaScript la interviu [REZOLVAT]

La un interviu pe JavaScript am primit urmatoarea problema - a trebuit sa construiesc clasa Deferred asa incat sa am output-ul de mai jos daca se ruleaza codul:

let d = new Deferred();

d.then(res => { console.log("1 ", res); return "a"; });
d.then(res => { console.log("2 ", res); return "b"; });
d.then(res => { console.log("3 ", res); return "c"; });

d.resolve("hello"); 

//output after d.resolve("hello") runs
//1 hello
//2 a
//3 b

iar solutia arata asa - practic salveaza functiile intr-un array, apoi le calluieste individual si returnul la una e parametru pentru urmatoarea:

class Deferred {
    constructor() {
        this.arrayFn = []
    }
    resolve(msg) {
        let res = msg;
        this.arrayFn.forEach((v, i) => {
            res = v(res)
        })
    }
    then(fn) {
        this.arrayFn.push(fn)
    }
}

dupa aceea s-a introdus o mica modificare la cerinta problemei asa incat ea arata asa:

let d = new Deferred();

d.then(res => {
    console.log("1 ", res);
    let d1 = new Deferred();
    setTimeout(() => d1.resolve("a"), 1500);
    return d1;
});
d.then(res => { console.log("2 ", res); return "b"; });
d.then(res => { console.log("3 ", res); return "c"; });

d.resolve("hello"); 

// output after d.resolve("hello") run
// 1 hello
// 1.5s later.. the rest should be printed
// 2 a
// 3 b

Plecand de la prima solutie, cum ati modifica clasa?

Un inceput de solutie pentru metoda resolve() ar fi mai jos :

resolve(msg) {
        let res = msg;
        this.arrayFn.forEach((v, i) => {
            if (res instanceof Deferred) {
                .....
            } else res = v(res)
        })
    }

N-o sa mearga niciodata ce vrei tu sa faci acolo cu setTimeout pentru ca mai intai o sa se afiseze cele de jos dupa care val cu timeout. Incearca sa folosesti setTimeout ca sa rezolvi un promise si poti sa astepti dupa el cu un await.

poate nu inteleg eu dar mi se pare stupid ce cer, ei vor ca set timeout sa fie blocking

1 Like

Ceea ce trebuie facut este sa se construiasca clasa Deffered. Restul este cerinta problemei si nu o poti schimba.

Incerc sa imi dau seama ce vor.
Posibil vor sa vada daca stii de call stack si event loop in JS, cred ca aici vor sa ajunga din interviu. Imi amintesc ca exista o solutie foarte eleganta cu generators.

setTimeout(() => d1.resolve(“a”), 1500); va fi pe propriul call stack, d1.resolve(“a”), 1500) e callback-ul lui.
Restul codului va fi pe alt call stack.

d1.resolve() cand il chemi e un callback, care va fi chemat cand se termina call-stack ul la setTimeout.

Ai o instanta de deferred, aici ai putea sa faci un singleton sa iti usurezi putin gandirea, sa nu ai doua instante intr-un singura, adica prima data returnezi mereu aceeasi instanta daca exista deja. Pe tine te intereseaza doar ca la fiecare instantiere sa tii evidenta fiecarui callback daca s-a rezolvat sau nu.

Ai un stack de callback-uri si valori, valorile le poti salva, dar callback-urile sunt mai complicate:

d1.resolve("a"), 1500) - cb1
 b - valoare la cb1
 c - valoare la cb1
d.resolve("hello") -cb0
 hello - valoare la cb0

Resolve e callback-ul tau care va face toata magia.

De fiecare data cand instantiezi Deferred(), tii evidenta fiecarui callback ca sa stii cate raspunsuri iti trebuie. Cand chemi resolve, schimbi evidenta din resolving in resolved si verifici daca mai sunt resolving, daca nu mai sunt resolving returnezi raspunsul de la callback-uri in ordinea instantierii lor.

1 Like

Eu cu javascript-ul nu prea le am, deci nu garantez că se poate/e corect/e frumos ce o să zic, dar gândul mă duce la Promise. Eu mă gândeam că în Deferred să mai ții un promise + un resolver pe care le inițializezi în constructor:

this.promise = new Promise((resolve, _) => { this.resolver = resolve; });

Iar în Deferred.resolve:

if (this.arrayFn.length == 0) {
   resolver(param);
   return param;
}
this.arrayFn.forEach((v, i) => {
  if (res instanceof Deferred) {
      res = v(await res.promise); // nu știu dacă se poate
   } else {
      res = v(res);
   }
});

resolver(res);
return res;

Again, nu știu dacă are sens per total, sper să ajute măcar ca idee

Are sens, pare ca suntem mai aproape de solutie

class Deferred {
    constructor() {
        this.arrayFn = []
        this.promise = new Promise((resolve, _) => { this.resolver = resolve; });
    }
    resolve(msg) {
        let res = msg;
        if (this.arrayFn.length == 0) {
            this.resolver(res);
            return res;
        }
        this.arrayFn.forEach(async(v, i) => {
            if (res instanceof Deferred) {
                res = v(await res.promise);
            } else {
                res = v(res)
            }
        })
    }
    then(fn) {
        this.arrayFn.push(fn)
    }
}

Returneaza

// 1  hello
// astepata 1.5 secunde
// 2  a
// 3  a
1 Like

Da, solutia ta necesita un promise intr-un fel sau altul. Dar nu sunt sigur daca vor sa folosesti new Promise () din ES6.

Eu n-am vazut Deferred in JS de cand fac programare.

Că e design-ul questionable sunt de acord, dar nu știu dacă la interviu se așteaptă la un răspuns de genul “e o idee stupidă” sau vor o soluție oricât de hackish ar fi.

class Deferred {
  constructor() {
    this.fns = [];
  }
  
  then(fn) {
    this.fns.push(fn);
  }
  
  resolve(arg) {
    this.next(arg);
  }
  
  next(arg) {
    if (!this.fns.length) {
      return;
    }
    
    let param = this.fns.shift()(arg);
    
    if (param instanceof Deferred) {
      param.then((res) => this.next(res));
    } else {
      this.next(param);
    }
  }
}

let now = null;
const log = (...args) => {
  const newNow = performance.now();
  console.log(parseFloat((newNow - now)/1000).toFixed(2) + 's', ...args);
  now = newNow;
}

let d = new Deferred();

now = performance.now();
d.then(res => {
    log("1 ", res);
    let d1 = new Deferred();
    setTimeout(() => d1.resolve("a"), 1500);
    return d1;
});
d.then(res => { log("2 ", res); return "b"; });
d.then(res => { log("3 ", res); return "c"; });

d.resolve("hello"); 

// output after d.resolve("hello") run
// 1 hello
// 1.5s later.. the rest should be printed
// 2 a

// our similar output ->

// 0.00s 1  hello
// 1.51s 2  a
// 0.00s 3  b

Solutia este foarte simpla si cred ca am nimerit perfect ce vor sa ‘atinga’ aici, in speta refolosirea acelui .then() in cazul in care functia returneaza un Deferred.

In solutia de mai sus, incepem prin a initializa un array care va contine toate functiile care vor fi apelate in momentul in care se apeleaza .resolve(). .then() va pune functia respectiva in acel array. Cream o functie .resolve() care apeleaza functia urmatoare din array (numita .next()), in cazul in care array-ul nu este gol. Atat functia .resolve() cat si functia .next() primesc un parametru care va fi transmis la acele functii.

Catch-ul este ca in functia .next() ne uitam la acel parametru returnat de functia curenta, iar daca acela este un Deferred (in exemplu nostru ‘d1’), atunci ii atasam o functie cu .then() (pentru ca find de tipul Deffered suporta acest lucru) in care apelam .next()-ul obiectului Deferred al nostru (in exemplu ‘d’). E un pic de inception aici, insa trebuie sa te gandesti ca e un fel de ‘forEach’ sau ‘while’, varianta asincron. Practic in loc sa se reia urmatorul ciclu din ‘while’, facem noi acest lucru prin acel .then() strategic.

Daca ai intrebari sa imi spui/spuneti.

5 Likes

Ei nu vor ca setTimeout sa fie blocking, vor sa se implementeze clasa Deferred. ‘Mosnegii’ de peste 30 de ani (ca mine) care au lucrat cu jQuery isi amintesc de ‘clasa’ Deferred din jQuery :stuck_out_tongue:

Ce vor ei in cerinta de mai sus (varianta simplificata) e ca niste functii sa se execute in ordine. Ca sa fiu mai specific, functiile care sunt in acele .then()-uri.

1 Like

Ca să evit await-ul ăla mă gândeam și eu să folosesc recursion dar îmi era greu să îmi imaginez cum îl fac totuși să aștepte

Folosind callback-uri, mama tuturor chestiilor asincrone in JavaScript. Orice ai avea, daca e eveniment de click in browser, un setTimeout (sau setInterval, sau requestAnimationFrame etc) ai pe undeva un callback (adica ‘da-mi un telefon cand se intampla actiunea’). In acel callback in exemplul de mai sus (si in multe altele) se reia flow-ul (chemi urmatoarea functie care asteapta la rand).

3 Likes

Mi se pare interesantă ideea cu acest “recursion” că pare că sunt corutine implementate de mână. Totuși acum mă gândesc, nu cumva e folosit JS într-un mod în care nu prea e conceput să fie? În alte limbaje e mai simplu cu așteptatul pentru că ai primitive mai low-level și poți avea și thread-uri și se poate implementa acel wait un pic mai ușor.

Still fun though.

Nu stiu ce sa zic, personal mi se par callback-urile cele mai usoare de inteles. Partea cu Promise-urile este defapt un sugar syntax peste callback-uri oricum. Adica codul pare sync, dar nu e. Am si lucrat cu JS si Node in special atat de mult incat imi vin foarte natural acum.

Defapt am scris niste librarii pentru control flow async cu callback-uri:

Acum in retrospectiva … vad ca sunt cam multe :smiley:

3 Likes

Ah, sper să nu se fi înțeles că am zis că e ciudat să se folosească callback-uri în JS, mă refeream strict la ce cer ei la acest interviu, ca să implementezi pare că trebuie să te cam lupți cu limbajul. Dar poate să fie și doar lipsa mea de experiență :slight_smile:

Nu as zice ca te lupti cu limbajul, dar nici nu e o intrebare usoara. Implementarea poate parea simpla, dar si mie personal mi-a luat ceva timp pana am inteles cum sta treaba cu ‘asynchronous JavaScript’. Daca ai facut lucruri normale pe partea de frontend nu te confrunti neaparat cu situatii de genul asta.

Partea practica la ‘teoria’ de mai sus exista, de ex: ti se cere sa construiesti o aplicatie single-page care trebuie sa faca N request-uri catre backend (cu fetch de exemplu) in paralel (daca N > 5 atunci limiteaza requesturile in paralel la maxim 5 in orice moment), iar cand acestea sunt gata mai trebuiesc inca 2 in serie. Apoi adauga un mecanism de retry, apoi de timeout. Fun stuff.

1 Like

Pe mine recent chiar m-a prins chestia asta cu async stuff, doar că eu sunt mai familiar cu lumea C++, cu JS nu prea-s familiar, dar oricum conceptele din ce știu se păstrează, și acolo am început să mai implementez primitve ca să mai învăț, e interesant să vezi cum se așază toate task-urile frumos și câtă performanță poți să ai pe un singur thread. Chiar acum am în plan să fac un async runtime (cât pot eu) în care să implementez și chestia asta cu așteptatul pentru mai multe concurrently (un fel de Promise.all).

Da, in aplicatiile web (si nu numai) de multe ori astepti dupa I/O (request-uri catre alte servere, interogari baze de date etc), asa ca aplicatiile nu sunt CPU-heavy. Astfel incat Node.js se preteaza foarte bine la ele chiar daca ‘actiunea’ se desfasoara pe un singur thread (defapt nu e pe un singur thread, insa codul tau - in care ai bagat si callbackurile - este).

Pentru aplicatiile care necesita CPU intens, se pot folosi alte limbaje ca si C++ sau Rust.

1 Like

Ok, cred că am înțeles cum funcționează, dar chestia care mă roade pe mine (de asta folosisem await):

Acel resolve se execută asincron? Adică de exemplu dacă aș vrea să folosesc rezultatul ultimului callback ar trebui să mai pun un .then, nu aș putea să zic ceva de genul:

d.then(() => 5);
d.resolve() + 3;

S-ar putea implementa asta pentru interfața pe care o cer ei?

LE: nevermind, I’m dumb. Abia acum am văzut că atunci când e un Deferred se apelează next pe this, nu pe acel Deferred, nu știu la ce mă gândeam. Sau nu are legătură pentru că .then doar dă un push? Ahhh, cred că mă bag la culcare și mă uit mâine că mă bate acum :laughing: