Правильный способ написания циклов для обещаний.


116

Как правильно построить цикл, чтобы убедиться, что следующий вызов обещания и связанный logger.log (res) выполняются синхронно через итерацию? (Bluebird)

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

Я пробовал следующий способ (метод из 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');
}); 

Хотя вроде работает, но я не думаю, что это гарантирует порядок вызова logger.log (res);

Какие-либо предложения?


1
Код мне кажется прекрасным (рекурсия с loopфункцией - это способ создания синхронных циклов). Как вы думаете, почему нет гарантии?
— hugomg 09

db.getUser (email) гарантированно будет вызываться по порядку. Но поскольку db.getUser () сам по себе является обещанием, его последовательный вызов не обязательно означает, что запросы к базе данных для «электронной почты» выполняются последовательно из-за асинхронной функции обещания. Таким образом, logger.log (res) вызывается в зависимости от того, какой запрос завершится первым.
— user2127480 09

1
@ user2127480: Но следующая итерация цикла вызывается последовательно только после выполнения обещания, вот как whileработает этот код?
— Bergi

Ответы:


78

Я не думаю, что это гарантирует порядок вызова logger.log (res);

На самом деле это так. Этот оператор выполняется перед resolveвызовом.

Какие-либо предложения?

Много. Самым важным является использование вами антипаттерна create-обещание-вручную - просто делайте только

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

Во-вторых, эту whileфункцию можно значительно упростить:

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

В-третьих, я бы использовал не whileцикл (с закрывающей переменной), а forцикл:

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

2
К сожалению. За исключением того, что actionпринимает в valueкачестве аргумента в promiseFor. ТАК не позволил бы мне сделать такое небольшое изменение. Спасибо, это очень полезно и элегантно.
— Гордон

1
@ Roamer-1888: Может быть, терминология немного странная, но я имею в виду, что whileцикл действительно проверяет какое-то глобальное состояние, в то время как forцикл имеет свою итерационную переменную (счетчик), привязанную к самому телу цикла. Фактически я использовал более функциональный подход, который больше похож на итерацию фиксированной точки, чем на цикл. Еще раз проверьте их код, valueпараметр другой.
— Bergi

2
Хорошо, теперь я это вижу. Поскольку .bind()новое запутывается value, я думаю, что я мог бы выбрать функцию от руки для удобства чтения. И извините, если я слишком толстый, но если promiseForи promiseWhileне сосуществуют, то как одно может назвать другое?
— Roamer-1888

2
@herve Вы можете в основном опустить его и заменить return …на return Promise.resolve(…). Если вам нужны дополнительные меры защиты от исключения conditionили actionсоздания исключения (например, Promise.methodпредоставляет его ), оберните все тело функции вreturn Promise.resolve().then(() => { … })
— Берги

2
@herve На самом деле это должно быть Promise.resolve().then(action).…или Promise.resolve(action()).…, вам не нужно оборачивать возвращаемое значениеthen
— Берги

134

Если вам действительно нужна общая promiseWhen()функция для этой и других целей, то обязательно сделайте это, используя упрощения Берги. Однако из-за того, как работают обещания, передача обратных вызовов таким образом обычно не нужна и заставляет вас прыгать через сложные маленькие обручи.

Насколько я могу судить, вы пытаетесь:

  • для асинхронного получения ряда сведений о пользователе для набора адресов электронной почты (по крайней мере, это единственный сценарий, который имеет смысл).
  • сделать это путем построения .then()цепочки с помощью рекурсии.
  • для сохранения исходного порядка при обработке возвращенных результатов.

Определенная таким образом, проблема на самом деле является той, которая обсуждается в разделе «Сборник Kerfuffle» в Promise Anti-patterns , который предлагает два простых решения:

  • параллельные асинхронные вызовы с использованием Array.prototype.map()
  • последовательные асинхронные вызовы с использованием Array.prototype.reduce().

Параллельный подход (прямо) выявит проблему, которую вы пытаетесь избежать, - порядок ответов неопределен. Последовательный подход построит требуемую .then()цепочку - плоскую - без рекурсии.

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

Звоните следующим образом:

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

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

Как видите, нет необходимости в уродливой внешней переменной var countили связанной с ней conditionфункции. Предел (из 10 в вопросе) полностью определяется длиной массива arrayOfEmailAddys.


16
кажется, что это должен быть выбранный ответ. изящный и очень многоразовый подход.
— Кен

1
Кто-нибудь знает, передается ли улов обратно родителю? Например, если db.getUser завершится с ошибкой, будет ли ошибка (отклонения) повторяться?
— wayofthefuture

@wayofthefuture, нет. Подумайте об этом так ... вы не можете изменить историю.
— Бродяга-1888

4
Спасибо за ответ. Это должен быть принятый ответ.
— klvs

1
@ Roamer-1888 Моя ошибка, я неправильно понял исходный вопрос. Я (лично) искал решение, в котором исходный список, который вам нужен для сокращения, растет по мере урегулирования ваших запросов (это queryMore of DB). В этом случае я обнаружил, что идея использовать сокращение с генератором является довольно хорошим разделением (1) условного расширения цепочки обещаний и (2) потребления возвращенных результатов.
— jhp 07

40

Вот как я это делаю со стандартным объектом 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
— Jam

3
как отправить параметры таким образом?
— Akash

4
@khan в строке chain = chain.then (func), вы можете сделать либо: chain = chain.then(func.bind(null, "...your params here")); либо chain = chain.then(() => func("your params here"));
— youngwerth

9

Дано

  • функция asyncFn
  • массив предметов

необходимые

  • обещание цепочки .then () последовательно (по порядку)
  • родной es6

Решение

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())

2
Если asyncон собирается стать зарезервированным словом в JavaScript, это может добавить ясности переименование этой функции здесь.
— hippietrail 06

Кроме того, разве функции толстой стрелки без тела в фигурных скобках просто не возвращают то, что выражается в выражении? Это сделало бы код более лаконичным. Я также могу добавить комментарий о том, что currentон не используется.
— hippietrail 06

2
это правильный путь!
— teleme.io

4

Есть новый способ решить эту проблему - использовать async / await.

async function myFunction() {
  while(/* my condition */) {
    const res = await db.getUser(email);
    logger.log(res);
  }
}

myFunction().then(() => {
  /* do other stuff */
})

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function https://ponyfoo.com/articles/understanding-javascript-async-await


Спасибо, это не связано с использованием фреймворка (bluebird).
— Rolf

3

Предлагаемая Берги функция действительно хороша:

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

Тем не менее, я хочу сделать небольшое дополнение, которое имеет смысл при использовании обещаний:

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

Таким образом, цикл while может быть встроен в цепочку обещаний и разрешается с помощью lastValue (также, если action () никогда не запускается). См. Пример:

var count = 10;
util.promiseWhile(
  function condition() {
    return count > 0;
  },
  function action() {
    return new Promise(function(resolve, reject) {
      count = count - 1;
      resolve(count)
    })
  },
  count)

3

Я бы сделал что-то вроде этого:

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

Таким образом, dataAll представляет собой упорядоченный массив всех элементов для регистрации. И операция журнала будет выполняться, когда все обещания будут выполнены.


Promise.all одновременно вызовет promises. Таким образом, порядок завершения может измениться. Вопрос требует связанных обещаний. Так что порядок завершения не следует менять.
— canbax

Изменить 1: вам вообще не нужно вызывать Promise.all. Пока обещания запущены, они будут выполняться параллельно.
— canbax

1

Используйте async и 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'));
}

}

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

0

Как насчет того, чтобы использовать BlueBird ?

function fetchUserDetails(arr) {
    return Promise.each(arr, function(email) {
        return db.getUser(email).done(function(res) {
            logger.log(res);
        });
    });
}

0

Вот еще один метод (ES6 w / std Promise). Использует критерии выхода типа lodash / подчеркивания (return === false). Обратите внимание, что вы можете легко добавить метод exitIf () в параметры для запуска в 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)
    })
};

0

Использование стандартного объекта обещания, при котором обещание возвращает результаты.

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

0

Сначала возьмите массив обещаний (массив обещаний), а затем разрешите этот массив обещаний с помощью 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
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.