วิธีที่ถูกต้องในการเขียนลูปสำหรับสัญญา


116

วิธีสร้างลูปอย่างถูกต้องเพื่อให้แน่ใจว่าการเรียกคำสัญญาต่อไปนี้และlogger.log (res) ที่ถูกล่ามโซ่ทำงานพร้อมกันผ่านการวนซ้ำ? (คราม)

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

db.getUser (อีเมล) รับประกันว่าจะถูกเรียกตามลำดับ แต่เนื่องจาก db.getUser () เป็นสัญญาการเรียกมันตามลำดับจึงไม่ได้หมายความว่าการสืบค้นฐานข้อมูลสำหรับ 'อีเมล' จะทำงานตามลำดับเนื่องจากคุณสมบัติของสัญญาแบบอะซิงโครนัส ดังนั้นจึงมีการเรียก logger.log (res) ขึ้นอยู่กับว่าแบบสอบถามใดเกิดขึ้นจนเสร็จก่อน
user2127480

1
@ user2127480: แต่การวนซ้ำครั้งต่อไปจะเรียกว่าตามลำดับหลังจากที่สัญญาได้รับการแก้ไขแล้วนั่นคือวิธีการwhileทำงานของรหัสนั้น?
Bergi

คำตอบ:


78

ฉันไม่คิดว่ามันรับประกันลำดับการเรียก logger.log (res);

ที่จริงมันไม่ คำสั่งนั้นจะดำเนินการก่อนการresolveโทร

ข้อเสนอแนะใด ๆ ?

จำนวนมาก สิ่งที่สำคัญที่สุดคือการใช้antipattern แบบสร้างสัญญาด้วยตนเอง - เพียงแค่ทำเท่านั้น

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. ดังนั้นจะไม่ให้ฉันทำการแก้ไขเล็กน้อย ขอบคุณมันเป็นประโยชน์และสง่างามมาก
Gordon

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(() => { … })
Bergi

2
@herve จริงๆแล้วควรจะเป็นPromise.resolve().then(action).…หรือPromise.resolve(action()).…คุณไม่จำเป็นต้องห่อผลตอบแทนของthen
Bergi

134

หากคุณต้องการpromiseWhen()ฟังก์ชั่นทั่วไปสำหรับสิ่งนี้และวัตถุประสงค์อื่น ๆ ให้ทำโดยใช้การทำให้เข้าใจง่ายของ Bergi อย่างไรก็ตามเนื่องจากวิธีการให้คำมั่นสัญญาโดยทั่วไปการส่งการติดต่อกลับด้วยวิธีนี้จึงไม่จำเป็นและบังคับให้คุณต้องกระโดดผ่านห่วงเล็ก ๆ ที่ซับซ้อน

เท่าที่ฉันบอกได้ว่าคุณกำลังพยายาม:

  • เพื่อดึงชุดรายละเอียดผู้ใช้สำหรับชุดที่อยู่อีเมลแบบอะซิงโครนัส (อย่างน้อยนั่นก็เป็นสถานการณ์เดียวที่เหมาะสม)
  • ทำได้โดยการสร้าง.then()ห่วงโซ่ผ่านการเรียกซ้ำ
  • เพื่อรักษาคำสั่งเดิมเมื่อจัดการกับผลลัพธ์ที่ส่งคืน

กำหนดไว้แล้วปัญหานี้เป็นปัญหาที่กล่าวถึงภายใต้ "The Collection Kerfuffle" ในรูปแบบการต่อต้านสัญญาซึ่งเสนอวิธีแก้ปัญหาง่ายๆสองวิธี:

  • การโทรแบบอะซิงโครนัสแบบขนานโดยใช้ 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
รู้สึกว่านี่ควรเป็นคำตอบที่เลือก วิธีการที่สง่างามและใช้ซ้ำได้มาก
ken

1
ไม่มีใครรู้ว่าสิ่งที่จับได้จะแพร่กระจายกลับไปยังผู้ปกครองหรือไม่? ตัวอย่างเช่นหาก db.getUser ล้มเหลวข้อผิดพลาด (ปฏิเสธ) จะเผยแพร่การสำรองข้อมูลหรือไม่
wayofthefuture

@wayofthefuture เลขที่ คิดอย่างนี้ ..... คุณเปลี่ยนประวัติศาสตร์ไม่ได้
Roamer-1888

4
ขอบคุณสำหรับคำตอบ. นี่ควรเป็นคำตอบที่ได้รับการยอมรับ
klvs

1
@ Roamer-1888 ความผิดพลาดของฉันฉันอ่านคำถามเดิมผิด ฉัน (ส่วนตัว) กำลังมองหาวิธีแก้ปัญหาที่รายการ intial ที่คุณต้องการสำหรับการลดกำลังเพิ่มขึ้นเมื่อคำขอของคุณได้รับการชำระ (เป็นแบบสอบถามเพิ่มเติมของฐานข้อมูล) ในกรณีนี้ฉันพบว่าแนวคิดที่จะใช้การลดด้วยเครื่องกำเนิดไฟฟ้าเป็นการแยก (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)

Great answer @youngwerth
Jam Risser

3
วิธีส่งพารามิเตอร์ด้วยวิธีนี้?
Akash khan

4
@khan on the chain = chain.then (func) line คุณสามารถทำอย่างใดอย่างหนึ่ง: chain = chain.then(func.bind(null, "...your params here")); หรือ chain = chain.then(() => func("your params here"));
youngwerth

9

ป.ร. ให้ไว้

  • asyncFn ฟังก์ชัน
  • อาร์เรย์ของรายการ

จำเป็นต้องใช้

  • สัญญาผูกมัด. จากนั้น () ในชุด (ตามลำดับ)
  • 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/understand-javascript-async-await


ขอบคุณสิ่งนี้ไม่เกี่ยวข้องกับการใช้เฟรมเวิร์ก (bluebird)
Rolf

3

ฟังก์ชั่นที่แนะนำของ Bergi นั้นดีมาก:

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 จะโทรตามจะเรียกสัญญาในเวลาเดียวกัน ดังนั้นลำดับความสำเร็จอาจเปลี่ยนไป คำถามขอคำสัญญาที่ถูกล่ามโซ่ ดังนั้นไม่ควรเปลี่ยนลำดับความสำเร็จ
canbax

แก้ไข 1: คุณไม่จำเป็นต้องโทรหา Promise.all เลย ตราบเท่าที่คำสัญญาถูกยกเลิกพวกเขาจะดำเนินการควบคู่กันไป
canbax


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 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
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.