Rozwiązać obietnice jedna po drugiej (tj. W sekwencji)?


269

Rozważ następujący kod, który odczytuje tablicę plików w sposób szeregowy / sekwencyjny. readFileszwraca obietnicę, która jest rozwiązywana dopiero po odczytaniu wszystkich plików po kolei.

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  return new Promise((resolve, reject) => 

    var readSequential = function(index) {
      if (index >= files.length) {
        resolve();
      } else {
        readFile(files[index]).then(function() {
          readSequential(index + 1);
        }).catch(reject);
      }
    };

   readSequential(0); // Start!

  });
};

Powyższy kod działa, ale nie lubię rekurencji, aby rzeczy występowały sekwencyjnie. Czy istnieje prostszy sposób, aby ten kod można było ponownie napisać, aby nie musiałem używać mojej dziwnej readSequentialfunkcji?

Początkowo próbowałem użyć Promise.all, ale to spowodowało, że wszystkie readFilepołączenia były wykonywane jednocześnie, co nie jest tym, czego chcę:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};

2
Wszystko, co musi czekać na zakończenie poprzedniej operacji asynchronicznej, musi być wykonane w trybie wywołania zwrotnego. Korzystanie z obietnic tego nie zmienia. Potrzebujesz rekurencji.
Barmar

1
Do twojej wiadomości, to nie jest technicznie rekurencja, ponieważ nie ma gromadzenia ramek stosu. Poprzedni readFileSequential()powrócił już przed wywołaniem następnego (ponieważ jest asynchroniczny, kończy się długo po zwróceniu pierwotnego wywołania funkcji).
jfriend00

1
@ jfriend00 Akumulacja ramki stosu nie jest wymagana dla rekurencji - tylko samodzielne odniesienie. Jest to jednak tylko kwestia techniczna.
Benjamin Gruenbaum

3
@BenjaminGruenbaum - mam na myśli to, że absolutnie nie ma nic złego w tym, że samo wywołanie funkcji uruchamia następną iterację. Ma to zero wad i w rzeczywistości jest to skuteczny sposób sekwencjonowania operacji asynchronicznych. Nie ma więc powodu, aby unikać czegoś, co wygląda jak rekurencja. Istnieją rekurencyjne rozwiązania niektórych nieefektywnych problemów - nie jest to jeden z nich.
jfriend00

1
Hej, na dyskusję i prośbę w pokoju JavaScript Zredagowałem tę odpowiedź, abyśmy mogli wskazać innym jako kanoniczne. Jeśli się nie zgadzasz, daj mi znać, a ja go przywrócę i otworzę osobny.
Benjamin Gruenbaum

Odpowiedzi:


337

Aktualizacja 2017 : użyłbym funkcji asynchronicznej, jeśli środowisko ją obsługuje:

async function readFiles(files) {
  for(const file of files) {
    await readFile(file);
  }
};

Jeśli chcesz, możesz odroczyć odczytywanie plików, dopóki ich nie potrzebujesz, za pomocą generatora asynchronicznego (jeśli Twoje środowisko je obsługuje):

async function* readFiles(files) {
  for(const file of files) {
    yield await readFile(file);
  }
};

Aktualizacja: W drugim przemyśleniu - zamiast tego mogę użyć pętli for:

var readFiles = function(files) {
  var p = Promise.resolve(); // Q() in q

  files.forEach(file =>
      p = p.then(() => readFile(file)); 
  );
  return p;
};

Lub bardziej kompaktowo, z redukcją:

var readFiles = function(files) {
  return files.reduce((p, file) => {
     return p.then(() => readFile(file));
  }, Promise.resolve()); // initial
};

W innych bibliotekach obietnic (takich jak when i Bluebird) masz do tego odpowiednie narzędzia.

Na przykład Bluebird to:

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));

var readAll = Promise.resolve(files).map(fs.readFileAsync,{concurrency: 1 });
// if the order matters, you can use Promise.each instead and omit concurrency param

readAll.then(function(allFileContents){
    // do stuff to read files.
});

Chociaż tak naprawdę nie ma powodu, aby nie korzystać z asynchronizacji, czekamy na dziś.


2
@ EmreTapcı, nie. Funkcja strzałki „=>” już oznacza powrót.
Maks.

Jeśli używasz TypeScript, myślę, że najlepsze jest rozwiązanie „for in”. Zmniejsz zwroty rekurencyjnych obietnic np. pierwszy typ zwrotu połączenia to Obietnica <unikaj>, następnie drugi to Obietnica <Obietnica <unikaj>> i tak dalej - nie można pisać bez użycia żadnego
Artur Tagisow

@ArturTagisow TypeScript (przynajmniej nowe wersje) mają typy rekurencyjne i powinny poprawnie rozwiązywać typy tutaj. Nie ma czegoś takiego jak Obietnica <Obietnica <T>>, ponieważ obietnice „rekurencyjnie się asymilują”. Promise.resolve(Promise.resolve(15))jest identyczny z Promise.resolve(15).
Benjamin Gruenbaum


72

Oto jak wolę uruchamiać zadania w szeregu.

function runSerial() {
    var that = this;
    // task1 is a function that returns a promise (and immediately starts executing)
    // task2 is a function that returns a promise (and immediately starts executing)
    return Promise.resolve()
        .then(function() {
            return that.task1();
        })
        .then(function() {
            return that.task2();
        })
        .then(function() {
            console.log(" ---- done ----");
        });
}

Co ze skrzynkami z większą liczbą zadań? Jak 10?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}

8
A co z przypadkami, w których nie znasz dokładnej liczby zadań?
cholera

1
A jeśli znasz liczbę zadań, ale tylko w czasie wykonywania?
joeytwiddle

10
„w ogóle nie chcesz obsługiwać szeregu obietnic. Zgodnie ze specyfikacją obietnicy, gdy tylko obietnica zostanie utworzona, zaczyna ona działać. Tak więc naprawdę potrzebujesz szeregu fabryk obietnic”, patrz Błąd zaawansowany nr 3 tutaj: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

5
Jeśli chcesz zmniejszyć szumy na linii, możesz także napisaćresult = result.then(task);
Daniel Buckmaster

1
@DanielBuckmaster tak, ale bądź ostrożny, ponieważ jeśli task () zwróci wartość, zostanie ona przekazana do następnego wywołania. Jeśli twoje zadanie ma opcjonalne argumenty, może to powodować skutki uboczne. Aktualny kod połyka wyniki i jawnie wywołuje następne zadanie bez argumentów.
JHH

63

To pytanie jest stare, ale żyjemy w świecie ES6 i funkcjonalnego JavaScript, więc zobaczmy, jak możemy to poprawić.

Ponieważ obietnice wykonują się natychmiast, nie możemy po prostu stworzyć szeregu obietnic, wszystkie uruchomiłyby się równolegle.

Zamiast tego musimy stworzyć tablicę funkcji, która zwróci obietnicę. Każda funkcja zostanie następnie wykonana sekwencyjnie, co rozpocznie obietnicę w środku.

Możemy to rozwiązać na kilka sposobów, ale moim ulubionym sposobem jest użycie reduce.

To staje się trochę trudne reducew połączeniu z obietnicami, więc podzieliłem jedną wkładkę na kilka mniejszych strawnych ugryzień poniżej.

Istotą tej funkcji jest użycie reducezaczynając od wartości początkowej Promise.resolve([])lub obietnicy zawierającej pustą tablicę.

Ta obietnica zostanie następnie przekazana do reducemetody as promise. Jest to klucz do sekwencyjnego łączenia każdej obietnicy. Następną obietnicą do wykonania jest, funca kiedy thenpożary wyniki są konkatenowane, a następnie ta obietnica jest zwracana, wykonując reducecykl z funkcją następnej obietnicy.

Po wykonaniu wszystkich obietnic zwrócona obietnica będzie zawierać tablicę wszystkich wyników każdej obietnicy.

Przykład ES6 (jedna wkładka)

/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]))

Przykład ES6 (w podziale)

// broken down to for easier understanding

const concat = list => Array.prototype.concat.bind(list)
const promiseConcat = f => x => f().then(concat(x))
const promiseReduce = (acc, x) => acc.then(promiseConcat(x))
/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs => funcs.reduce(promiseReduce, Promise.resolve([]))

Stosowanie:

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const funcs = urls.map(url => () => $.ajax(url))

// execute them serially
serial(funcs)
    .then(console.log.bind(console))

1
bardzo dobrze, dziękuję, Array.prototype.concat.bind(result)to część, której mi brakowało, musiałem ręcznie naciskać na wyniki, które działały, ale działały mniej fajnie
zavr

Ponieważ chodzi nam o nowoczesny JS, uważam, że console.log.bind(console)stwierdzenie z twojego ostatniego przykładu jest zwykle niepotrzebne. Te dni możesz po prostu przejść console.log. Na przykład. serial(funcs).then(console.log). Testowane na obecnych nodejs i Chrome.
Molomby,

To było trochę trudne do obejścia, ale redukcja zasadniczo robi to poprawnie? Promise.resolve([]).then((x) => { const data = mockApi('/data/1'); return Promise.resolve(x.concat(data)) }).then((x) => { const data = mockApi('/data/2'); return Promise.resolve(x.concat(data)); });
danecando

@danecando, tak, to wygląda poprawnie. Możesz również upuścić Promise.resolve w zamian, wszelkie zwrócone wartości zostaną automatycznie rozwiązane, chyba że wywołasz na nich Promise.reject.
joelnet

@joelnet, w odpowiedzi na komentarz danecando, myślę, że redukcja powinna być bardziej poprawna ekspres w następującym wyrażeniu, zgadzasz się? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))i tak dalej
przepełnienie bufora76

37

Aby to zrobić po prostu w ES6:

function(files) {
  // Create a new empty promise (don't do that with real people ;)
  var sequence = Promise.resolve();

  // Loop over each file, and add on a promise to the
  // end of the 'sequence' promise.
  files.forEach(file => {

    // Chain one computation onto the sequence
    sequence = 
      sequence
        .then(() => performComputation(file))
        .then(result => doSomething(result)); 
        // Resolves for each file, one at a time.

  })

  // This will resolve after the entire chain is resolved
  return sequence;
}

1
Wygląda na to, że używa podkreślnika. Możesz uprościć, files.forEachczy pliki są tablicą.
Gustavo Rodrigues

2
Cóż ... to jest ES5. Sposób ES6 byłby for (file of files) {...}.
Gustavo Rodrigues

1
Mówisz, że nie powinieneś używać Promise.resolve()do tworzenia już rozwiązanej obietnicy w prawdziwym życiu. Dlaczego nie? Promise.resolve()wydaje się czystszy niż new Promise(success => success()).
canac

8
@canac Przepraszamy, to był tylko żart z grą słów („puste obietnice…”). Zdecydowanie użyj Promise.resolve();w swoim kodzie.
Shridhar Gupta,

1
Ładne rozwiązanie, łatwe do naśladowania. Nie return sequence;umieściłem mojego w funkcji, więc do rozwiązania na końcu zamiast wstawianiasequence.then(() => { do stuff });
Joe Coyle

25

Proste użycie dla standardowej obietnicy Node.js:

function sequence(tasks, fn) {
    return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve());
}

AKTUALIZACJA

items-promise to gotowy do użycia pakiet NPM, który robi to samo.


6
Chciałbym zobaczyć to wyjaśnione bardziej szczegółowo.
Tyguy7

Podałem odmianę tej odpowiedzi z wyjaśnieniem poniżej. Dzięki
Sarsaparilla,

Właśnie to robię w środowiskach sprzed Node 7, które nie mają dostępu do asynchronizacji / oczekiwania. Ładnie i czysto.
JHH

11

Musiałem uruchomić wiele sekwencyjnych zadań i wykorzystałem te odpowiedzi, aby stworzyć funkcję, która zająłaby się obsługą każdego sekwencyjnego zadania ...

function one_by_one(objects_array, iterator, callback) {
    var start_promise = objects_array.reduce(function (prom, object) {
        return prom.then(function () {
            return iterator(object);
        });
    }, Promise.resolve()); // initial
    if(callback){
        start_promise.then(callback);
    }else{
        return start_promise;
    }
}

Funkcja przyjmuje 2 argumenty + 1 opcjonalnie. Pierwszy argument to tablica, nad którą będziemy pracować. Drugi argument to samo zadanie, funkcja, która zwraca obietnicę, następne zadanie zostanie uruchomione dopiero po spełnieniu tej obietnicy. Trzeci argument to wywołanie zwrotne uruchamiane po wykonaniu wszystkich zadań. Jeśli nie zostanie przekazane żadne wywołanie zwrotne, funkcja zwróci utworzoną obietnicę, abyśmy mogli obsłużyć koniec.

Oto przykład użycia:

var filenames = ['1.jpg','2.jpg','3.jpg'];
var resize_task = function(filename){
    //return promise of async resizing with filename
};
one_by_one(filenames,resize_task );

Mam nadzieję, że zaoszczędzi komuś trochę czasu ...


Niesamowite rozwiązanie, było najlepsze, jakie znalazłem w ciągu prawie tygodnia zmagań .... Jest bardzo dobrze wyjaśnione, ma logiczne imiona wewnętrzne, dobry przykład (może być lepszy), mogę bezpiecznie wezwać tyle samo razy w razie potrzeby i zawiera opcję ustawiania oddzwaniania. po prostu ŁADNY! (Właśnie zmieniłem nazwę na coś, co czyni mnie bardziej sensownym) .... ZALECENIE dla innych ... możesz iterować obiekt za pomocą „Object.keys ( myObject )” jako swojego „object_array”
DavidTaubmann

Dzięki za komentarz! Nie używam też tej nazwy, ale chciałem, aby była ona bardziej oczywista / prosta.
Salketer

5

Najmilszym rozwiązaniem, które udało mi się wymyślić, były bluebirdobietnice. Możesz po prostu zrobić, Promise.resolve(files).each(fs.readFileAsync);które gwarancje, że obietnice są rozwiązywane sekwencyjnie w kolejności.


1
Nawet lepiej: Promise.each(filtes, fs.readFileAsync). Przy okazji, nie musisz tego robić .bind(fs)?
Bergi,

Wydaje się, że nikt tutaj nie rozumie różnicy między tablicą a sekwencją, która sugeruje nieograniczony / dynamiczny rozmiar.
vitaly-t

Zauważ, że tablice w JavaScript nie mają nic wspólnego z tablicami o stałym rozmiarze w językach C. Są tylko obiektami z włączonym zarządzaniem klawiszami numerycznymi i nie mają ustalonego rozmiaru ani limitu ( szczególnie nie przy użyciu new Array(int). Wszystko, co robi, to ustawienie lengthpary klucz-wartość, wpływające na liczbę wskaźników używanych podczas iteracji opartej na długości. Ma zero wpływ na faktyczne indeksowanie tablicy lub granice indeksu)
Mike 'Pomax' Kamermans

4

Jest to niewielka odmiana innej odpowiedzi powyżej. Korzystanie z natywnych obietnic:

function inSequence(tasks) {
    return tasks.reduce((p, task) => p.then(task), Promise.resolve())
}

Wyjaśnienie

Jeśli masz te zadania [t1, t2, t3], powyższe jest równoważne z Promise.resolve().then(t1).then(t2).then(t3). To jest zachowanie redukcji.

Jak używać

Najpierw musisz zbudować listę zadań! Zadanie to funkcja, która nie przyjmuje argumentów. Jeśli potrzebujesz przekazać argumenty do swojej funkcji, użyj bindlub innych metod, aby utworzyć zadanie. Na przykład:

var tasks = files.map(file => processFile.bind(null, file))
inSequence(tasks).then(...)

4

Moje preferowane rozwiązanie:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

Nie różni się zasadniczo od innych tutaj opublikowanych, ale:

  • Stosuje funkcję do elementów w szeregu
  • Rozwiązuje szereg wyników
  • Nie wymaga asynchronizacji / oczekiwania (wsparcie jest nadal dość ograniczone, około 2017 r.)
  • Używa funkcji strzałek; miły i zwięzły

Przykładowe użycie:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

Testowane na rozsądnym bieżącym Chrome (v59) i NodeJS (v8.1.2).


3

Użyj Array.prototype.reducei pamiętaj, aby zawrzeć obietnice w funkcji, w przeciwnym razie będą już działać!

// array of Promise providers

const providers = [
  function(){
     return Promise.resolve(1);
  },
  function(){
     return Promise.resolve(2);
  },
  function(){
     return Promise.resolve(3);
  }
]


const inSeries = function(providers){

  const seed = Promise.resolve(null); 

  return providers.reduce(function(a,b){
      return a.then(b);
  }, seed);
};

przyjemnie i łatwo ... powinieneś być w stanie ponownie użyć tego samego materiału siewnego do wydajności itp.

Ważne jest, aby chronić się przed pustymi tablicami lub tablicami zawierającymi tylko 1 element podczas korzystania z redukcji , więc ta technika jest najlepszym wyborem:

   const providers = [
      function(v){
         return Promise.resolve(v+1);
      },
      function(v){
         return Promise.resolve(v+2);
      },
      function(v){
         return Promise.resolve(v+3);
      }
    ]

    const inSeries = function(providers, initialVal){

        if(providers.length < 1){
            return Promise.resolve(null)
        }

        return providers.reduce((a,b) => a.then(b), providers.shift()(initialVal));
    };

a następnie nazwij to tak:

inSeries(providers, 1).then(v => {
   console.log(v);  // 7
});

2

Stworzyłem tę prostą metodę na obiekcie Promise:

Utwórz i dodaj metodę Promise.sequence do obiektu Promise

Promise.sequence = function (chain) {
    var results = [];
    var entries = chain;
    if (entries.entries) entries = entries.entries();
    return new Promise(function (yes, no) {
        var next = function () {
            var entry = entries.next();
            if(entry.done) yes(results);
            else {
                results.push(entry.value[1]().then(next, function() { no(results); } ));
            }
        };
        next();
    });
};

Stosowanie:

var todo = [];

todo.push(firstPromise);
if (someCriterium) todo.push(optionalPromise);
todo.push(lastPromise);

// Invoking them
Promise.sequence(todo)
    .then(function(results) {}, function(results) {});

Najlepsze w tym rozszerzeniu obiektu Promise jest to, że jest ono zgodne ze stylem obietnic. Promise.all i Promise.sequence są wywoływane w ten sam sposób, ale mają inną semantykę.

Uwaga

Sekwencyjne uruchamianie obietnic zwykle nie jest bardzo dobrym sposobem korzystania z obietnic. Zwykle lepiej jest użyć Promise.all i pozwolić przeglądarce na uruchomienie kodu tak szybko, jak to możliwe. Istnieją jednak rzeczywiste przypadki użycia - na przykład podczas pisania aplikacji mobilnej za pomocą javascript.


Nie, nie możesz porównywać Promise.alli swojego Promise.sequence. Jeden przyjmuje iterowalną obietnicę, drugi przyjmuje szereg funkcji, które zwracają obietnice.
Bergi,

Przy okazji, polecam unikać obietnicy konstruktora antipattern
Bergi

Nie wiedziałem, że potrzeba iteratora. Powinny być jednak łatwe do przepisania. Czy możesz wyjaśnić, dlaczego jest to antypattern konstruktora obietnic? Przeczytałem twój post tutaj: stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi Zaktualizowałem kod, aby obsługiwał iteratory. Nadal nie widzę, że to jest antypattern. Antypatterny należy zasadniczo traktować jako wytyczne, aby uniknąć błędów w kodowaniu, i doskonale jest tworzyć funkcje (biblioteczne), które łamią te wytyczne.
frodeborli

Tak, jeśli uważasz, że jest to funkcja biblioteczna, jest OK, ale w tym przypadku reduceodpowiedź w odpowiedzi Benjamina jest o wiele prostsza.
Bergi,

2

Możesz użyć tej funkcji, która dostaje listę promiseFactories:

function executeSequentially(promiseFactories) {
    var result = Promise.resolve();
    promiseFactories.forEach(function (promiseFactory) {
        result = result.then(promiseFactory);
    });
    return result;
}

Promise Factory to prosta funkcja, która zwraca obietnicę:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Działa, ponieważ fabryka obietnic nie tworzy obietnicy, dopóki nie zostanie o to poproszona. Działa tak samo jak funkcja ów - w rzeczywistości jest to to samo!

Nie chcesz w ogóle działać w oparciu o szereg obietnic. Zgodnie ze specyfikacją Promise, zaraz po utworzeniu promesy, zaczyna się ona wykonywać. Więc tak naprawdę chcesz szeregu obiecujących fabryk ...

Jeśli chcesz dowiedzieć się więcej o Obietnicach, sprawdź ten link: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

Moja odpowiedź na podstawie https://stackoverflow.com/a/31070150/7542429 .

Promise.series = function series(arrayOfPromises) {
    var results = [];
    return arrayOfPromises.reduce(function(seriesPromise, promise) {
      return seriesPromise.then(function() {
        return promise
        .then(function(result) {
          results.push(result);
        });
      });
    }, Promise.resolve())
    .then(function() {
      return results;
    });
  };

To rozwiązanie zwraca wyniki jako tablicę, taką jak Promise.all ().

Stosowanie:

Promise.series([array of promises])
.then(function(results) { 
  // do stuff with results here
});

2

Bardzo podobała mi się odpowiedź @ joelnet, ale dla mnie ten styl kodowania jest nieco trudny do przyswojenia, więc spędziłem kilka dni, próbując wymyślić, jak wyraziłbym to samo rozwiązanie w bardziej czytelny sposób, a to jest mój weź tylko inną składnię i kilka komentarzy.

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const functions = urls.map((url) => {
  // For every url we return a new function
  return () => {
    return new Promise((resolve) => {
      // random wait in milliseconds
      const randomWait = parseInt((Math.random() * 1000),10)
      console.log('waiting to resolve in ms', randomWait)
      setTimeout(()=>resolve({randomWait, url}),randomWait)
    })
  }
})


const promiseReduce = (acc, next) => {
  // we wait for the accumulator to resolve it's promise
  return acc.then((accResult) => {
    // and then we return a new promise that will become
    // the new value for the accumulator
    return next().then((nextResult) => {
      // that eventually will resolve to a new array containing
      // the value of the two promises
      return accResult.concat(nextResult)
    })
  })
};
// the accumulator will always be a promise that resolves to an array
const accumulator = Promise.resolve([])

// we call reduce with the reduce function and the accumulator initial value
functions.reduce(promiseReduce, accumulator)
  .then((result) => {
    // let's display the final value here
    console.log('=== The final result ===')
    console.log(result)
  })

2

Jak zauważył Bergi, myślę, że najlepszym i przejrzystym rozwiązaniem jest użycie BlueBird.each, kod poniżej:

const BlueBird = require('bluebird');
BlueBird.each(files, fs.readFileAsync);

2

Po pierwsze, musisz zrozumieć, że obietnica zostaje wykonana w momencie stworzenia.
Na przykład jeśli masz kod:

["a","b","c"].map(x => returnsPromise(x))

Musisz to zmienić na:

["a","b","c"].map(x => () => returnsPromise(x))

Następnie musimy sekwencyjnie łączyć obietnice:

["a", "b", "c"].map(x => () => returnsPromise(x))
    .reduce(
        (before, after) => before.then(_ => after()),
        Promise.resolve()
    )

wykonanie after(), upewni się, że obietnica zostanie utworzona (i wykonana) tylko wtedy, gdy nadejdzie odpowiedni czas.


1

Używam następującego kodu, aby rozszerzyć obiekt Promise. Obsługuje odrzucanie obietnic i zwraca szereg wyników

Kod

/*
    Runs tasks in sequence and resolves a promise upon finish

    tasks: an array of functions that return a promise upon call.
    parameters: an array of arrays corresponding to the parameters to be passed on each function call.
    context: Object to use as context to call each function. (The 'this' keyword that may be used inside the function definition)
*/
Promise.sequence = function(tasks, parameters = [], context = null) {
    return new Promise((resolve, reject)=>{

        var nextTask = tasks.splice(0,1)[0].apply(context, parameters[0]); //Dequeue and call the first task
        var output = new Array(tasks.length + 1);
        var errorFlag = false;

        tasks.forEach((task, index) => {
            nextTask = nextTask.then(r => {
                output[index] = r;
                return task.apply(context, parameters[index+1]);
            }, e=>{
                output[index] = e;
                errorFlag = true;
                return task.apply(context, parameters[index+1]);
            });
        });

        // Last task
        nextTask.then(r=>{
            output[output.length - 1] = r;
            if (errorFlag) reject(output); else resolve(output);
        })
        .catch(e=>{
            output[output.length - 1] = e;
            reject(output);
        });
    });
};

Przykład

function functionThatReturnsAPromise(n) {
    return new Promise((resolve, reject)=>{
        //Emulating real life delays, like a web request
        setTimeout(()=>{
            resolve(n);
        }, 1000);
    });
}

var arrayOfArguments = [['a'],['b'],['c'],['d']];
var arrayOfFunctions = (new Array(4)).fill(functionThatReturnsAPromise);


Promise.sequence(arrayOfFunctions, arrayOfArguments)
.then(console.log)
.catch(console.error);

1

Jeśli chcesz, możesz użyć funkcji zmniejsz do złożenia sekwencyjnej obietnicy, na przykład:

[2,3,4,5,6,7,8,9].reduce((promises, page) => {
    return promises.then((page) => {
        console.log(page);
        return Promise.resolve(page+1);
    });
  }, Promise.resolve(1));

zawsze będzie działać sekwencyjnie.


1

Korzystanie z nowoczesnego ES:

const series = async (tasks) => {
  const results = [];

  for (const task of tasks) {
    const result = await task;

    results.push(result);
  }

  return results;
};

//...

const readFiles = await series(files.map(readFile));

1

Z Async / Await (jeśli masz wsparcie ES7)

function downloadFile(fileUrl) { ... } // This function return a Promise

async function main()
{
  var filesList = [...];

  for (const file of filesList) {
    await downloadFile(file);
  }
}

(musisz użyć forpętli, a nie forEachdlatego, że async / await ma problemy z uruchomieniem w pętli forEach)

Bez asynchronizacji / oczekiwania (przy użyciu obietnicy)

function downloadFile(fileUrl) { ... } // This function return a Promise

function downloadRecursion(filesList, index)
{
  index = index || 0;
  if (index < filesList.length)
  {
    downloadFile(filesList[index]).then(function()
    {
      index++;
      downloadRecursion(filesList, index); // self invocation - recursion!
    });
  }
  else
  {
    return Promise.resolve();
  }
}

function main()
{
  var filesList = [...];
  downloadRecursion(filesList);
}

2
Oczekiwanie wewnątrz forEach nie jest zalecane.
Marcelo Agimóvel

@ MarceloAgimóvel - Zaktualizowałem rozwiązanie, aby nie współpracować forEach(zgodnie z tym )
Gil Epshtain

0

Na podstawie tytułu pytania „Rozwiąż obietnice jeden po drugim (tj. W sekwencji)?”, Możemy zrozumieć, że OP jest bardziej zainteresowany sekwencyjną obsługą obietnic przy rozliczeniu niż sekwencyjnymi wezwaniami per se .

Ta odpowiedź jest oferowana:

  • w celu wykazania, że ​​sekwencyjne wywołania nie są konieczne do sekwencyjnego przetwarzania odpowiedzi.
  • ujawnienia realnych alternatywnych wzorców odwiedzającym tę stronę - w tym PO, jeśli nadal będzie zainteresowany ponad rok później.
  • pomimo twierdzenia OP, że nie chce on wykonywać połączeń jednocześnie, co może rzeczywiście mieć miejsce, ale równie dobrze może być założeniem opartym na chęci sekwencyjnego przetwarzania odpowiedzi, jak sugeruje tytuł.

Jeśli równoczesne połączenia nie są naprawdę pożądane, zapoznaj się z odpowiedzią Benjamina Gruenbauma, która obejmuje kompleksowo połączenia sekwencyjne (itp.).

Jeśli jednak jesteś zainteresowany (dla lepszej wydajności) wzorcami, które pozwalają na jednoczesne połączenia, a następnie sekwencyjną obsługę odpowiedzi, czytaj dalej.

Kuszące jest, aby pomyśleć, że musisz użyć Promise.all(arr.map(fn)).then(fn)(jak już wiele razy) lub fantazyjnego cukru z Promise lib (zwłaszcza Bluebird), jednak (dzięki temu artykułowi ) arr.map(fn).reduce(fn)wzór spełni swoje zadanie, z zaletami, które:

  • działa tylko z każdą obietnicą lib - nawet z wcześniejszymi wersjami jQuery - tylko .then() używana jest .
  • zapewnia elastyczność pomijania przekroczenia błędu lub zatrzymania po błędzie, w zależności od tego, co chcesz za pomocą modu z jedną linią.

Oto jest napisane dla Q.

var readFiles = function(files) {
    return files.map(readFile) //Make calls in parallel.
    .reduce(function(sequence, filePromise) {
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//skip-over-error. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Uwaga: tylko ten jeden fragment, Q() jest specyficzny dla Q. W przypadku jQuery musisz upewnić się, że readFile () zwraca obietnicę jQuery. Z bibliotekami A + obietnice zagraniczne zostaną zasymilowane.

Kluczem tutaj jest sequenceobietnica redukcji , która porządkuje postępowanie z readFileobietnicami, ale nie ich tworzenie.

A kiedy już to zrozumiesz, może być nieco oszałamiające, gdy zdasz sobie sprawę, że .map()scena nie jest tak naprawdę konieczna! Całe zadanie, połączenia równoległe i obsługa szeregowa w prawidłowej kolejności, można wykonać reduce()samodzielnie, a także dodatkową zaletą dodatkowej elastyczności w zakresie:

  • konwertuj z równoległych wywołań asynchronicznych na szeregowe wywołania asynchroniczne, po prostu przesuwając jedną linię - potencjalnie przydatne podczas programowania.

Oto Qznowu.

var readFiles = function(files) {
    return files.reduce(function(sequence, f) {
        var filePromise = readFile(f);//Make calls in parallel. To call sequentially, move this line down one.
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//Skip over any errors. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

To jest podstawowy wzór. Jeśli chcesz również dostarczyć dane (np. Pliki lub ich transformację) do dzwoniącego, potrzebujesz łagodnego wariantu.


Nie sądzę, że dobrym pomysłem jest odpowiadanie na pytania sprzeczne z intencjami PO…
Bergi

1
To sequence.then(() => filePromise)jest anty-wzór - nie propaguje błędów tak szybko, jak to możliwe (i tworzy unhandledRejectionw bibliotekach, które je obsługują). Raczej powinieneś użyć Q.all([sequence, filePromise])lub $.when(sequence, filePromise). Wprawdzie takie zachowanie może być tym, czego chcesz, gdy chcesz ignorować lub pomijać błędy, ale powinieneś przynajmniej wymienić to jako wadę.
Bergi

@Bergi, mam nadzieję, że OP wkroczy i wyda osąd, czy jest to naprawdę sprzeczne z jego intencjami, czy nie. Jeśli nie, usunę chyba odpowiedź, tymczasem mam nadzieję, że uzasadniłem swoje stanowisko. Dziękujemy za potraktowanie tego na tyle poważnie, aby zapewnić przyzwoitą informację zwrotną. Czy możesz wyjaśnić więcej na temat anty-wzoru lub podać referencję? Czy to samo dotyczy artykułu, w którym znalazłem podstawowy wzór ?
Roamer-1888,

1
Tak, trzecia wersja jego kodu (czyli „zarówno równoległy, jak i sekwencyjny”) ma ten sam problem. „Antypattern” wymaga zaawansowanej obsługi błędów i ma skłonność do asynchronicznego dołączania programów obsługi, co powoduje unhandledRejectionzdarzenia. W Bluebird możesz obejść ten problem, używając sequence.return(filePromise)tego samego zachowania, ale dobrze radzi sobie z odrzucaniem. Nie znam żadnego odniesienia, właśnie go wymyśliłem - nie sądzę, by „(anty) wzór” miał jeszcze nazwę.
Bergi,

1
@Bergi, możesz wyraźnie zobaczyć coś, czego nie mogę :( Zastanawiam się, czy ten nowy anty-wzór wymaga gdzieś udokumentowania?
Roamer-1888,

0

Twoje podejście nie jest złe, ale ma dwa problemy: połyka błędy i stosuje jawną obietnicę konstrukcyjną Antipattern.

Możesz rozwiązać oba te problemy i sprawić, że kod będzie czystszy, nadal stosując tę ​​samą ogólną strategię:

var Q = require("q");

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  var readSequential = function(index) {
    if (index < files.length) {
      return readFile(files[index]).then(function() {
        return readSequential(index + 1);
      });
    }
  };

  // using Promise.resolve() here in case files.length is 0
  return Promise.resolve(readSequential(0)); // Start!
};

0

Jeśli ktoś inny potrzebuje gwarantowanego ŚCIEŻNIE sekwencyjnego sposobu rozwiązywania obietnic podczas wykonywania operacji CRUD, możesz również użyć następującego kodu jako podstawy.

Tak długo, jak dodasz „return” przed wywołaniem każdej funkcji, opisując obietnicę, i wykorzystaj ten przykład jako podstawę do następnego wywołania funkcji .then () KONSEKWENTNIE rozpocznie się po zakończeniu poprzedniej:

getRidOfOlderShoutsPromise = () => {
    return readShoutsPromise('BEFORE')
    .then(() => {
        return deleteOlderShoutsPromise();
    })
    .then(() => {
        return readShoutsPromise('AFTER')
    })
    .catch(err => console.log(err.message));
}

deleteOlderShoutsPromise = () => {
    return new Promise ( (resolve, reject) => {
        console.log("in deleteOlderShouts");
        let d = new Date();
        let TwoMinuteAgo = d - 1000 * 90 ;
        All_Shouts.deleteMany({ dateTime: {$lt: TwoMinuteAgo}}, function(err) {
            if (err) reject();
            console.log("DELETED OLDs at "+d);
            resolve();        
        });
    });
}

readShoutsPromise = (tex) => {
    return new Promise( (resolve, reject) => {
        console.log("in readShoutsPromise -"+tex);
        All_Shouts
        .find({})
        .sort([['dateTime', 'ascending']])
        .exec(function (err, data){
            if (err) reject();
            let d = new Date();
            console.log("shouts "+tex+" delete PROMISE = "+data.length +"; date ="+d);
            resolve(data);
        });    
    });
}

0

Do sekwencji obietnic można zastosować metodę push i pop tablic. Możesz także przekazywać nowe obietnice, gdy potrzebujesz dodatkowych danych. To jest kod, którego użyję w React Infinite loader do ładowania sekwencji stron.

var promises = [Promise.resolve()];

function methodThatReturnsAPromise(page) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log(`Resolve-${page}! ${new Date()} `);
			resolve();
		}, 1000);
	});
}

function pushPromise(page) {
	promises.push(promises.pop().then(function () {
		return methodThatReturnsAPromise(page)
	}));
}

pushPromise(1);
pushPromise(2);
pushPromise(3);


0

Większość odpowiedzi nie zawiera wyników WSZYSTKICH obietnic indywidualnie, więc w przypadku, gdy ktoś szuka tego konkretnego zachowania, jest to możliwe rozwiązanie za pomocą rekurencji.

Jest zgodny ze stylem Promise.all:

  • Zwraca tablicę wyników w .then()wywołaniu zwrotnym.

  • Jeśli jakaś obietnica się nie powiedzie, zostanie natychmiast zwrócona w .catch()wywołaniu zwrotnym.

const promiseEach = (arrayOfTasks) => {
  let results = []
  return new Promise((resolve, reject) => {
    const resolveNext = (arrayOfTasks) => {
      // If all tasks are already resolved, return the final array of results
      if (arrayOfTasks.length === 0) return resolve(results)

      // Extract first promise and solve it
      const first = arrayOfTasks.shift()

      first().then((res) => {
        results.push(res)
        resolveNext(arrayOfTasks)
      }).catch((err) => {
        reject(err)
      })
    }
    resolveNext(arrayOfTasks)
  })
}

// Lets try it 😎

const promise = (time, shouldThrowError) => new Promise((resolve, reject) => {
  const timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    if (shouldThrowError) reject(new Error('Promise failed'))
    resolve(time)
  }, timeInMs)
})

const tasks = [() => promise(1), () => promise(2)]

promiseEach(tasks)
  .then((res) => {
    console.log(res) // [1, 2]
  })
  // Oops some promise failed
  .catch((error) => {
    console.log(error)
  })

Uwaga na temat tasksdeklaracji tablicowej :

W takim przypadku nie jest możliwe użycie następującej notacji, takiej jak Promise.all:

const tasks = [promise(1), promise(2)]

I musimy użyć:

const tasks = [() => promise(1), () => promise(2)]

Powodem jest to, że JavaScript zaczyna wykonywać obietnicę natychmiast po jej ogłoszeniu. Jeśli użyjemy takich metod Promise.all, po prostu sprawdza, czy stan wszystkich z nich jest fulfilledlub rejected, ale nie uruchamia samej ekekcji. Używając () => promise()tego zatrzymujemy wykonywanie aż do jego wywołania.


0
(function() {
  function sleep(ms) {
    return new Promise(function(resolve) {
      setTimeout(function() {
        return resolve();
      }, ms);
    });
  }

  function serial(arr, index, results) {
    if (index == arr.length) {
      return Promise.resolve(results);
    }
    return new Promise(function(resolve, reject) {
      if (!index) {
        index = 0;
        results = [];
      }
      return arr[index]()
        .then(function(d) {
          return resolve(d);
        })
        .catch(function(err) {
          return reject(err);
        });
    })
      .then(function(result) {
        console.log("here");
        results.push(result);
        return serial(arr, index + 1, results);
      })
      .catch(function(err) {
        throw err;
      });
  }

  const a = [5000, 5000, 5000];

  serial(a.map(x => () => sleep(x)));
})();

Tutaj kluczem jest sposób wywołania funkcji uśpienia. Musisz przekazać tablicę funkcji, która sama zwraca obietnicę zamiast tablicy obietnic.


-1

Ma to na celu wyjaśnienie, w jaki sposób przetwarzać sekwencję obietnic w bardziej ogólny sposób, obsługując sekwencje dynamiczne / nieskończone, w oparciu o implementację spex.sequence :

var $q = require("q");
var spex = require('spex')($q);

var files = []; // any dynamic source of files;

var readFile = function (file) {
    // returns a promise;
};

function source(index) {
    if (index < files.length) {
        return readFile(files[index]);
    }
}

function dest(index, data) {
    // data = resolved data from readFile;
}

spex.sequence(source, dest)
    .then(function (data) {
        // finished the sequence;
    })
    .catch(function (error) {
        // error;
    });

Nie tylko to rozwiązanie będzie działać z sekwencjami dowolnej wielkości, ale możesz łatwo dodawać do niego ograniczanie danych i równoważenie obciążenia .

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.