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


116

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

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

Я спробував наступним чином (метод від http://blog.victorquinn.com/javascript-promise-time-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

db.getUser (електронна пошта) гарантовано викликається по порядку. Але, оскільки сам db.getUser () є обіцянкою, виклик його послідовно не обов'язково означає, що запити бази даних для 'електронної пошти' запускаються послідовно через асинхронну особливість обіцянки. Таким чином, logger.log (res) викликається залежно від того, який запит повинен закінчитися першим.
user2127480

1
@ user2127480: Але наступна ітерація циклу викликається послідовно лише після того, як обіцянка вирішена, ось як whileпрацює цей код?
Бергі

Відповіді:


78

Я не думаю, що це гарантує порядок виклику logger.log (res);

Власне, це і є. Ця заява виконується перед resolveвикликом.

Будь-які пропозиції?

Багато. Найголовніше - це використовувати антипатрій створення - обіцяти - вручну - лише робити це

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параметр інший.
Бергі

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
Bergi

134

Якщо ви дійсно хочете отримати загальну promiseWhen()функцію для цієї та інших цілей, то, безумовно, зробіть це, використовуючи спрощення Бергі. Однак через спосіб, який обіцяє роботу, передача зворотних дзвінків таким чином, як правило, непотрібна і змушує перестрибувати маленькі складні обручі.

Наскільки я можу сказати, ви намагаєтесь:

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

Визначена таким чином, проблема є фактично тією, яку обговорювали у розділі "Колекція Kerfuffle" в Promise Anti- pattern , яка пропонує два простих рішення:

  • паралельні асинхронні дзвінки з використанням 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, ні. Подумайте про це так ..... ви не можете змінити історію.
Roamer-1888

4
Дякую за відповідь. Це має бути прийнятою відповіддю.
klvs

1
@ Roamer-1888 Моя помилка, я неправильно прочитав оригінальне запитання. Я (особисто) розглядав рішення, де список необхідних для зменшення розростається, коли ваші запити вирішуються (це запитБальшої бази даних). У цьому випадку я знайшов ідею використовувати зменшити з генератором досить приємне розмежування (1) умовного розширення ланцюга обіцянок та (2) споживання повернених результатів.
jhp

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 Risser

3
як надсилати парами таким чином?
Акаш хан

4
@khan на ланцюжку line = chain.then (func), ви можете зробити або: chain = chain.then(func.bind(null, "...your params here")); або chain = chain.then(() => func("your params here"));
молодняк

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

Крім того, чи не так, що жирова стрілка функціонує без тіла в дужках, просто повертає те, що оцінює вираз там? Це зробило б код більш стислим. Я також можу додати коментар із зазначенням, що currentвін не використовується.
hippietrail

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


Дякую, це не передбачає використання рамки (синій птах).
Рольф

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 (також якщо дія () ніколи не виконується). Див. Приклад:

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 зателефонує заклик, який обіцяє одночасно. Тож порядок заповнення може змінитися. Питання задає ланцюгові обіцянки. Тож порядок заповнення не слід змінювати.
canbax

Редагувати 1: Вам взагалі не потрібно дзвонити на Promise.all Поки обіцянки будуть виконані, вони будуть виконуватися паралельно.
canbax

1

Використовуйте асинхронізацію та очікуйте (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

Ось ще один метод (ES6 w / std Обіцяння). Використовує критерії виходу типів подання / підкреслення (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.