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


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.