Prawidłowy sposób zapisywania pętli dla obietnicy.

116

Jak poprawnie skonstruować pętlę, aby upewnić się, że następuje wywołanie obietnicy i łańcuch logger.log (res) działają synchronicznie przez iterację? (niebieski ptak)

db.getUser(email).then(function(res) { logger.log(res); }); // this is a promise

Spróbowałem w następujący sposób (metoda z http://blog.victorquinn.com/javascript-promise-while-loop )

var Promise = require('bluebird');

var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
});

var count = 0;
promiseWhile(function() {
    return count < 10;
}, function() {
    return new Promise(function(resolve, reject) {
        db.getUser(email)
          .then(function(res) { 
              logger.log(res); 
              count++;
              resolve();
          });
    }); 
}).then(function() {
    console.log('all done');
}); 

Chociaż wydaje się, że działa, ale nie sądzę, że gwarantuje kolejność wywoływania logger.log (res);

Jakieś sugestie?

user2127480
źródło
1
Kod wygląda dla mnie dobrze (rekurencja z loopfunkcją jest sposobem na wykonanie pętli synchronicznych). Jak myślisz, dlaczego nie ma żadnej gwarancji?
hugomg
db.getUser (e-mail) jest wywoływany w kolejności. Ponieważ jednak db.getUser () sama w sobie jest obietnicą, wywoływanie jej sekwencyjnie niekoniecznie oznacza, że ​​zapytania bazy danych o „e-mail” są wykonywane sekwencyjnie ze względu na asynchroniczną funkcję obietnicy. W ten sposób logger.log (res) jest wywoływany w zależności od tego, które zapytanie zakończy się jako pierwsze.
user2127480
1
@ user2127480: Ale następna iteracja pętli jest wywoływana sekwencyjnie dopiero po spełnieniu obietnicy, tak whiledziała ten kod?
Bergi

Odpowiedzi:

78

Myślę, że nie gwarantuje to kolejności wywoływania logger.log (res);

Właściwie tak. Ta instrukcja jest wykonywana przed resolvewywołaniem.

Jakieś sugestie?

Wiele. Najważniejsze jest, abyś używał antywzorca tworzenia-obietnicy-ręcznie - po prostu zrób tylko

promiseWhile(…, function() {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 count++;
             });
})…

Po drugie, tę whilefunkcję można bardzo uprościć:

var promiseWhile = Promise.method(function(condition, action) {
    if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Po trzecie, nie użyłbym whilepętli (ze zmienną zamknięcia), ale forpętli:

var promiseFor = Promise.method(function(condition, action, value) {
    if (!condition(value)) return value;
    return action(value).then(promiseFor.bind(null, condition, action));
});

promiseFor(function(count) {
    return count < 10;
}, function(count) {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 return ++count;
             });
}, 0).then(console.log.bind(console, 'all done'));
Bergi
źródło
2
Ups. Z wyjątkiem tego, że actionprzyjmuje valuejako argument promiseFor. WIĘC nie pozwoliłby mi zrobić tak małej edycji. Dzięki, jest bardzo pomocny i elegancki.
Gordon,
1
@ Roamer-1888: Może terminologia jest trochę dziwna, ale mam na myśli, że whilepętla testuje jakiś stan globalny, podczas gdy forpętla ma zmienną iteracyjną (licznik) przypisaną do samej treści pętli. W rzeczywistości zastosowałem bardziej funkcjonalne podejście, które bardziej przypomina iterację punktów stałych niż pętlę. Sprawdź ponownie ich kod, valueparametr jest inny.
Bergi
2
OK, teraz to widzę. Ponieważ .bind()zaciemniamy nowe value, myślę, że mógłbym wybrać tę funkcję odręcznie, aby była czytelna. I przepraszam, jeśli jestem gruba, ale jeśli promiseFori promiseWhilenie współistnieją, to jak jedno nazywa się drugim?
Roamer-1888
2
@herve można w zasadzie pominąć go i zastąpić return …przez return Promise.resolve(…). Jeśli potrzebujesz dodatkowych zabezpieczeń przed conditionlub actionwyrzucenia wyjątku (tak jak Promise.methodto zapewnia ), zawiń całą funkcję funkcji wreturn Promise.resolve().then(() => { … })
Bergi
2
@herve Właściwie to powinno być Promise.resolve().then(action).…lub Promise.resolve(action()).…, nie musisz zawijać wartości zwracanejthen
Bergi
134

Jeśli naprawdę chcesz generała promiseWhen() funkcję do tego i innych celów, zrób to za wszelką cenę, korzystając z uproszczeń Bergi. Jednak ze względu na sposób, w jaki działają obietnice, przekazywanie wywołań zwrotnych w ten sposób jest generalnie niepotrzebne i zmusza do przeskakiwania przez skomplikowane małe obręcze.

O ile wiem, próbujesz:

  • asynchroniczne pobieranie szeregu szczegółów użytkownika dla zbioru adresów e-mail (przynajmniej jest to jedyny sensowny scenariusz).
  • aby to zrobić, budując .then()łańcuch poprzez rekursję.
  • aby zachować oryginalną kolejność podczas obsługi zwróconych wyników.

Zdefiniowany w ten sposób problem jest w rzeczywistości omawiany w „The Collection Kerfuffle” w Promise Anti-patterns , która oferuje dwa proste rozwiązania:

  • równoległe wywołania asynchroniczne przy użyciu Array.prototype.map()
  • szeregowe wywołania asynchroniczne przy użyciu Array.prototype.reduce().

Podejście równoległe da (bezpośrednio) problem, którego starasz się uniknąć - kolejność odpowiedzi jest niepewna. Podejście szeregowe zbuduje wymagany .then()łańcuch - płaski - bez rekursji.

function fetchUserDetails(arr) {
    return arr.reduce(function(promise, email) {
        return promise.then(function() {
            return db.getUser(email).done(function(res) {
                logger.log(res);
            });
        });
    }, Promise.resolve());
}

Zadzwoń w następujący sposób:

//Compose here, by whatever means, an array of email addresses.
var arrayOfEmailAddys = [...];

fetchUserDetails(arrayOfEmailAddys).then(function() {
    console.log('all done');
});

Jak widać, nie ma potrzeby stosowania brzydkiej zmiennej zewnętrznej countani powiązanej z nią conditionfunkcji. Limit (w pytaniu 10) jest w całości określony przez długość tablicy arrayOfEmailAddys.

Roamer-1888
źródło
16
wydaje się, że to powinna być wybrana odpowiedź. wdzięczne i bardzo wielokrotnego użytku.
ken
1
Czy ktoś wie, czy złapanie rozprzestrzeni się z powrotem na rodzica? Na przykład, jeśli db.getUser zakończy się niepowodzeniem, czy błąd (odrzucenie) będzie propagowany z powrotem?
przyszłości
@wayofthefuture, nie. Pomyśl o tym w ten sposób ..... nie możesz zmienić historii.
Roamer-1888
4
Dziękuję za odpowiedź. To powinna być akceptowana odpowiedź.
klvs
1
@ Roamer-1888 Mój błąd, źle odczytałem pierwotne pytanie. Ja (osobiście) szukałem rozwiązania, w którym lista początkowa, którą potrzebujesz do redukcji, rośnie w miarę rozliczania się twoich żądań (jest to zapytanie więcej o DB). W tym przypadku wpadłem na pomysł, aby użyć redukuj z generatorem całkiem ładne oddzielenie (1) warunkowego przedłużenia łańcucha obietnic i (2) zużycia zwróconych rezultatów.
jhp
40

Oto, jak robię to ze standardowym obiektem Promise.

// Given async function sayHi
function sayHi() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('Hi');
      resolve();
    }, 3000);
  });
}

// And an array of async functions to loop through
const asyncArray = [sayHi, sayHi, sayHi];

// We create the start of a promise chain
let chain = Promise.resolve();

// And append each function in the array to the promise chain
for (const func of asyncArray) {
  chain = chain.then(func);
}

// Output:
// Hi
// Hi (After 3 seconds)
// Hi (After 3 more seconds)
Youngwerth
źródło
Świetna odpowiedź @youngwerth
Jam Risser
3
jak wysyłać parametry w ten sposób?
Akash khan
4
@khan on the chain = chain.then (func) line, możesz zrobić albo: chain = chain.then(func.bind(null, "...your params here")); lub chain = chain.then(() => func("your params here"));
youngwerth
9

Dany

  • funkcja asyncFn
  • tablica elementów

wymagany

  • obietnica łańcuchowa .then () jest szeregowo (w kolejności)
  • rodzimy es6

Rozwiązanie

let asyncFn = (item) => {
  return new Promise((resolve, reject) => {
    setTimeout( () => {console.log(item); resolve(true)}, 1000 )
  })
}

// asyncFn('a')
// .then(()=>{return async('b')})
// .then(()=>{return async('c')})
// .then(()=>{return async('d')})

let a = ['a','b','c','d']

a.reduce((previous, current, index, array) => {
  return previous                                    // initiates the promise chain
  .then(()=>{return asyncFn(array[index])})      //adds .then() promise for each item
}, Promise.resolve())
kamran
źródło
2
Jeśli asyncma stać się słowem zastrzeżonym w JavaScript, może to zwiększyć jasność, aby zmienić nazwę tej funkcji w tym miejscu.
hippietrail
Czy nie jest też tak, że gruba strzałka działa bez ciała w nawiasach klamrowych, po prostu zwraca to, do czego obliczane jest wyrażenie? To uczyniłoby kod bardziej zwięzłym. Mógłbym również dodać komentarz stwierdzający, że currentnie jest używany.
hippietrail
2
to jest właściwy sposób!
teleme.io
3

Sugerowana funkcja Bergi jest naprawdę fajna:

var promiseWhile = Promise.method(function(condition, action) {
      if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Nadal chcę zrobić drobny dodatek, który ma sens przy korzystaniu z obietnic:

var promiseWhile = Promise.method(function(condition, action, lastValue) {
  if (!condition()) return lastValue;
  return action().then(promiseWhile.bind(null, condition, action));
});

W ten sposób pętla while może zostać osadzona w łańcuchu obietnic i rozwiązana za pomocą lastValue (również jeśli akcja () nigdy nie zostanie uruchomiona). Zobacz przykład:

var count = 10;
util.promiseWhile(
  function condition() {
    return count > 0;
  },
  function action() {
    return new Promise(function(resolve, reject) {
      count = count - 1;
      resolve(count)
    })
  },
  count)
Patrick Wieth
źródło
3

Zrobiłbym coś takiego:

var request = []
while(count<10){
   request.push(db.getUser(email).then(function(res) { return res; }));
   count++
};

Promise.all(request).then((dataAll)=>{
  for (var i = 0; i < dataAll.length; i++) {

      logger.log(dataAll[i]); 
  }  
});

w ten sposób dataAll jest uporządkowaną tablicą wszystkich elementów do zarejestrowania. Operacja dziennika zostanie wykonana, gdy wszystkie obietnice zostaną spełnione.

Claudio
źródło
Promise.all zadzwoni do obietnic woli w tym samym czasie. Więc kolejność ukończenia może się zmienić. Pytanie dotyczy przykutych obietnic. Nie należy więc zmieniać kolejności wypełniania.
canbax,
Edycja 1: W ogóle nie musisz dzwonić do Promise.all. Dopóki obietnice zostaną zwolnione, będą wykonywane równolegle.
canbax
1

Użyj async i await (es6):

function taskAsync(paramets){
 return new Promise((reslove,reject)=>{
 //your logic after reslove(respoce) or reject(error)
})
}

async function fName(){
let arry=['list of items'];
  for(var i=0;i<arry.length;i++){
   let result=await(taskAsync('parameters'));
}

}
Ramandrareddy reddam
źródło
0
function promiseLoop(promiseFunc, paramsGetter, conditionChecker, eachFunc, delay) {
    function callNext() {
        return promiseFunc.apply(null, paramsGetter())
            .then(eachFunc)
    }

    function loop(promise, fn) {
        if (delay) {
            return new Promise(function(resolve) {
                setTimeout(function() {
                    resolve();
                }, delay);
            })
                .then(function() {
                    return promise
                        .then(fn)
                        .then(function(condition) {
                            if (!condition) {
                                return true;
                            }
                            return loop(callNext(), fn)
                        })
                });
        }
        return promise
            .then(fn)
            .then(function(condition) {
                if (!condition) {
                    return true;
                }
                return loop(callNext(), fn)
            })
    }

    return loop(callNext(), conditionChecker);
}


function makeRequest(param) {
    return new Promise(function(resolve, reject) {
        var req = https.request(function(res) {
            var data = '';
            res.on('data', function (chunk) {
                data += chunk;
            });
            res.on('end', function () {
                resolve(data);
            });
        });
        req.on('error', function(e) {
            reject(e);
        });
        req.write(param);
        req.end();
    })
}

function getSomething() {
    var param = 0;

    var limit = 10;

    var results = [];

    function paramGetter() {
        return [param];
    }
    function conditionChecker() {
        return param <= limit;
    }
    function callback(result) {
        results.push(result);
        param++;
    }

    return promiseLoop(makeRequest, paramGetter, conditionChecker, callback)
        .then(function() {
            return results;
        });
}

getSomething().then(function(res) {
    console.log('results', res);
}).catch(function(err) {
    console.log('some error along the way', err);
});
Tengiz
źródło
0

A co powiesz na ten z BlueBird ?

function fetchUserDetails(arr) {
    return Promise.each(arr, function(email) {
        return db.getUser(email).done(function(res) {
            logger.log(res);
        });
    });
}
sposób na przyszłość
źródło
0

Oto inna metoda (ES6 ze standardową obietnicą). Używa kryteriów wyjścia typu lodash / underscore (return === false). Zauważ, że możesz łatwo dodać metodę exitIf () do opcji uruchamianych w doOne ().

const whilePromise = (fnReturningPromise,options = {}) => { 
    // loop until fnReturningPromise() === false
    // options.delay - setTimeout ms (set to 0 for 1 tick to make non-blocking)
    return new Promise((resolve,reject) => {
        const doOne = () => {
            fnReturningPromise()
            .then((...args) => {
                if (args.length && args[0] === false) {
                    resolve(...args);
                } else {
                    iterate();
                }
            })
        };
        const iterate = () => {
            if (options.delay !== undefined) {
                setTimeout(doOne,options.delay);
            } else {
                doOne();
            }
        }
        Promise.resolve()
        .then(iterate)
        .catch(reject)
    })
};
GrumpyGary
źródło
0

Używanie standardowego obiektu obietnicy i zwracanie wyników przez obietnicę.

function promiseMap (data, f) {
  const reducer = (promise, x) =>
    promise.then(acc => f(x).then(y => acc.push(y) && acc))
  return data.reduce(reducer, Promise.resolve([]))
}

var emails = []

function getUser(email) {
  return db.getUser(email)
}

promiseMap(emails, getUser).then(emails => {
  console.log(emails)
})
Chris Blaser
źródło
0

Najpierw weź tablicę obietnic (tablicę obietnic), a po rozwiązaniu tablicy obietnic za pomocą Promise.all(promisearray).

var arry=['raju','ram','abdul','kruthika'];

var promiseArry=[];
for(var i=0;i<arry.length;i++) {
  promiseArry.push(dbFechFun(arry[i]));
}

Promise.all(promiseArry)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
     console.log(error);
  });

function dbFetchFun(name) {
  // we need to return a  promise
  return db.find({name:name}); // any db operation we can write hear
}
Ramandrareddy reddam
źródło