ฉันจะรอชุดฟังก์ชันการโทรกลับแบบอะซิงโครนัสได้อย่างไร


96

ฉันมีรหัสที่มีลักษณะเช่นนี้ใน javascript:

forloop {
    //async call, returns an array to its callback
}

หลังจากการเรียก async ทั้งหมดเสร็จสิ้นฉันต้องการคำนวณค่าต่ำสุดของอาร์เรย์ทั้งหมด

ฉันจะรอพวกเขาทั้งหมดได้อย่างไร?

ความคิดเดียวของฉันตอนนี้คือการมีอาร์เรย์ของบูลีนที่เรียกว่า done และตั้งค่า done [i] เป็น true ในฟังก์ชัน callback ith แล้วพูดว่า while (ยังไม่เสร็จทั้งหมด) {}

แก้ไข: ฉันคิดว่าวิธีแก้ปัญหาที่เป็นไปได้ แต่น่าเกลียดวิธีหนึ่งคือการแก้ไขอาร์เรย์ที่ทำเสร็จแล้วในการโทรกลับแต่ละครั้งจากนั้นเรียกใช้เมธอดหากมีการตั้งค่าอื่น ๆ ทั้งหมดจากการโทรกลับแต่ละครั้งดังนั้นการโทรกลับครั้งสุดท้ายที่จะเสร็จสมบูรณ์จะเรียกใช้วิธีการต่อเนื่อง

ขอบคุณล่วงหน้า.


1
ใน async คุณหมายถึงรอให้คำขอ Ajax เสร็จสมบูรณ์หรือไม่?
Peter Aron Zentai

7
หมายเหตุwhile (not all are done) { }จะไม่ทำงาน ในขณะที่คุณกำลังรอไม่ว่างการโทรกลับของคุณจะไม่ทำงาน
cHao

ใช่. ฉันกำลังรอการเรียก async ไปยัง API ภายนอกเพื่อส่งคืนเพื่อให้มันเริ่มการทำงานของวิธีการโทรกลับ ใช่เฉาฉันรู้แล้วจึงขอความช่วยเหลือที่นี่ D
codersarepeople

คุณสามารถลองสิ่งนี้: github.com/caolan/asyncชุดฟังก์ชันยูทิลิตี้ async ที่ดีมาก
Paul Greyson

คำตอบ:


193

คุณไม่ได้เจาะจงรหัสของคุณมากนักดังนั้นฉันจะสร้างสถานการณ์ สมมติว่าคุณมีการโทรของ ajax 10 ครั้งและคุณต้องการสะสมผลลัพธ์จากการโทรของ ajax 10 ครั้งจากนั้นเมื่อพวกเขาดำเนินการเสร็จสิ้นคุณก็ต้องการทำอะไรบางอย่าง คุณสามารถทำได้โดยการสะสมข้อมูลในอาร์เรย์และติดตามว่าข้อมูลสุดท้ายเสร็จสิ้นเมื่อใด:

เคาน์เตอร์คู่มือ

var ajaxCallsRemaining = 10;
var returnedData = [];

for (var i = 0; i < 10; i++) {
    doAjax(whatever, function(response) {
        // success handler from the ajax call

        // save response
        returnedData.push(response);

        // see if we're done with the last ajax call
        --ajaxCallsRemaining;
        if (ajaxCallsRemaining <= 0) {
            // all data is here now
            // look through the returnedData and do whatever processing 
            // you want on it right here
        }
    });
}

หมายเหตุ: การจัดการข้อผิดพลาดมีความสำคัญที่นี่ (ไม่แสดงเนื่องจากเป็นเรื่องเฉพาะสำหรับวิธีการโทรของคุณโดยใช้ ajax) คุณจะต้องคิดว่าคุณจะจัดการกับกรณีนี้อย่างไรเมื่อการโทรของ ajax ไม่สำเร็จไม่ว่าจะด้วยข้อผิดพลาดหรือติดขัดเป็นเวลานานหรือหมดเวลาหลังจากผ่านไปนาน


jQuery สัญญา

เพิ่มคำตอบของฉันในปี 2014 ทุกวันนี้คำสัญญามักใช้ในการแก้ปัญหาประเภทนี้เนื่องจาก jQuery $.ajax()ส่งคืนคำสัญญาไปแล้วและ$.when()จะแจ้งให้คุณทราบเมื่อกลุ่มของคำสัญญาได้รับการแก้ไขทั้งหมดและจะรวบรวมผลตอบแทนให้คุณ:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push($.ajax(...));
}
$.when.apply($, promises).then(function() {
    // returned data is in arguments[0][0], arguments[1][0], ... arguments[9][0]
    // you can process it here
}, function() {
    // error occurred
});

สัญญามาตรฐาน ES6

ตามที่ระบุไว้ในคำตอบของ kba : หากคุณมีสภาพแวดล้อมที่มีสัญญาดั้งเดิมในตัว (เบราว์เซอร์สมัยใหม่หรือ node.js หรือใช้ babeljs Transpile หรือใช้ polyfill คำสัญญา) คุณสามารถใช้สัญญาที่ระบุ ES6 ได้ ดูตารางนี้สำหรับการรองรับเบราว์เซอร์ สัญญาได้รับการสนับสนุนในเบราว์เซอร์ปัจจุบันเกือบทั้งหมดยกเว้น IE

หากdoAjax()คืนคำสัญญาคุณสามารถทำได้:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});

หากคุณจำเป็นต้องทำการดำเนินการ async ที่ไม่ใช่สัญญาให้เป็นแบบที่ส่งคืนสัญญาคุณสามารถ "ให้คำมั่นสัญญา" ได้ดังนี้:

function doAjax(...) {
    return new Promise(function(resolve, reject) {
        someAsyncOperation(..., function(err, result) {
            if (err) return reject(err);
            resolve(result);
        });
    });
}

จากนั้นใช้รูปแบบด้านบน:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});

บลูเบิร์ดสัญญา

หากคุณใช้ไลบรารีที่มีคุณลักษณะมากมายเช่นไลบรารีBluebird Promiseจะมีฟังก์ชันเพิ่มเติมบางอย่างในตัวเพื่อให้ง่ายขึ้น:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });

4
@kba - ฉันจะไม่เรียกคำตอบนี้ว่าล้าสมัยอย่างแน่นอนเนื่องจากเทคนิคทั้งหมดยังสามารถใช้ได้โดยเฉพาะอย่างยิ่งถ้าคุณใช้ jQuery สำหรับ Ajax แต่ฉันได้อัปเดตหลายวิธีเพื่อรวมคำสัญญาของเจ้าของภาษา
jfriend00

สมัยนี้มีวิธีแก้ปัญหาที่สะอาดกว่ามากซึ่งไม่จำเป็นต้องใช้ jquery ฉันกำลังทำกับ FetchAPI and Promises
philx_x

@philx_x - คุณกำลังทำอะไรเกี่ยวกับการสนับสนุน IE และ Safari
jfriend00

@ jfriend00 GitHub ทำ polyfill github.com/github/fetch หรือฉันไม่แน่ใจว่า babel รองรับการดึงข้อมูลหรือยัง babeljs.io
philx_x

@philx_x - คิดอย่างนั้น คุณต้องมีไลบรารี polyfill เพื่อใช้การดึงข้อมูลในปัจจุบัน ระบายความคิดเห็นของคุณเล็กน้อยเกี่ยวกับการหลีกเลี่ยงห้องสมุดของ ajax Fetch เป็นสิ่งที่ดี แต่ก็ห่างจากที่จะสามารถใช้งานได้โดยไม่ใช้ polyfill ยังไม่ได้อยู่ในเบราว์เซอร์เวอร์ชันล่าสุดทั้งหมด ทำมันไม่ได้เปลี่ยนอะไรเลยในคำตอบของฉัน ฉันมีdoAjax()ที่คืนคำสัญญาเป็นหนึ่งในตัวเลือก เช่นเดียวกับfetch().
jfriend00

18

เช็คอินตั้งแต่ปี 2015: ตอนนี้เรามีคำสัญญาดั้งเดิมในเบราว์เซอร์ล่าสุด (Edge 12, Firefox 40, Chrome 43, Safari 8, Opera 32 และเบราว์เซอร์ Android 4.4.4 และ iOS Safari 8.4 แต่ไม่ใช่ Internet Explorer, Opera Mini และเวอร์ชันเก่ากว่า ของ Android)

หากเราต้องการดำเนินการ 10 async และรับการแจ้งเตือนเมื่อการดำเนินการทั้งหมดเสร็จสิ้นเราสามารถใช้เนทีPromise.allฟได้โดยไม่ต้องมีไลบรารีภายนอก:

function asyncAction(i) {
    return new Promise(function(resolve, reject) {
        var result = calculateResult();
        if (result.hasError()) {
            return reject(result.error);
        }
        return resolve(result);
    });
}

var promises = [];
for (var i=0; i < 10; i++) {
    promises.push(asyncAction(i));
}

Promise.all(promises).then(function AcceptHandler(results) {
    handleResults(results),
}, function ErrorHandler(error) {
    handleError(error);
});

2
Promises.all()Promise.all()ควรจะเป็น
jfriend00

1
คำตอบของคุณต้องอ้างอิงถึงเบราว์เซอร์ที่คุณสามารถใช้ได้Promise.all()ซึ่งไม่มี IE เวอร์ชันปัจจุบัน
jfriend00

10

คุณสามารถใช้ออบเจ็กต์Deferredของ jQuery ร่วมกับเมธอดเมื่อ

deferredArray = [];
forloop {
    deferred = new $.Deferred();
    ajaxCall(function() {
      deferred.resolve();
    }
    deferredArray.push(deferred);
}

$.when(deferredArray, function() {
  //this code is called after all the ajax calls are done
});

7
คำถามไม่ได้ติดแท็กjQueryซึ่งมักจะหมายความว่า OP ไม่ต้องการคำตอบ jQuery
jfriend00

8
@ jfriend00 ฉันไม่ต้องการสร้างวงล้อใหม่เมื่อมันถูกสร้างขึ้นแล้วใน jQuery
พอล

4
@ พอลดังนั้นแทนที่จะประดิษฐ์วงล้อของคุณใหม่รวมถึงขยะ 40kb เพื่อทำอะไรง่ายๆ (รอการตัดบัญชี)
Raynos

2
แต่ไม่ใช่ทุกคนที่สามารถหรือต้องการใช้ jQuery และกำหนดเองที่นี่ใน SO คือคุณระบุว่าคุณแท็กคำถามของคุณด้วย jQuery หรือไม่
jfriend00

4
$. เมื่อเรียกเป็นตัวอย่างนี้ไม่ถูกต้อง ในการรออาร์เรย์รอการตัดบัญชี / สัญญาคุณต้องใช้ $ .when.apply ($, สัญญา). แล้ว (function () {/ * do stuff * /})
danw

9

คุณสามารถเลียนแบบได้ดังนี้:

  countDownLatch = {
     count: 0,
     check: function() {
         this.count--;
         if (this.count == 0) this.calculate();
     },
     calculate: function() {...}
  };

จากนั้นการเรียก async แต่ละครั้งจะทำสิ่งนี้:

countDownLatch.count++;

ในขณะที่ในการเรียก asynch แต่ละครั้งเมื่อสิ้นสุดวิธีการที่คุณเพิ่มบรรทัดนี้:

countDownLatch.check();

กล่าวอีกนัยหนึ่งคือคุณเลียนแบบฟังก์ชันการนับถอยหลังสลัก


ใน 99% ของกรณีการใช้งานทั้งหมด Promise คือหนทางที่จะไป แต่ฉันชอบคำตอบนี้เพราะมันแสดงให้เห็นถึงวิธีการจัดการโค้ด Async ในสถานการณ์ที่ Promise polyfill มีขนาดใหญ่กว่า JS ที่ใช้!
Sukima

6

นี่เป็นวิธีที่เรียบร้อยที่สุดในความคิดของฉัน

สัญญาทั้งหมด

FetchAPI

(ด้วยเหตุผลบางประการ Array.map ใช้ไม่ได้ภายในฟังก์ชันนั้นสำหรับฉัน แต่คุณสามารถใช้ a .forEach และ [] .concat () หรือสิ่งที่คล้ายกัน)

Promise.all([
  fetch('/user/4'),
  fetch('/user/5'),
  fetch('/user/6'),
  fetch('/user/7'),
  fetch('/user/8')
]).then(responses => {
  return responses.map(response => {response.json()})
}).then((values) => {
  console.log(values);
})

1
ผมคิดว่าความต้องการนี้จะเป็นหรือreturn responses.map(response => { return response.json(); }) return responses.map(response => response.json())

1

ใช้ไลบรารีโฟลว์ควบคุมเช่น after

after.map(array, function (value, done) {
    // do something async
    setTimeout(function () {
        // do something with the value
        done(null, value * 2)
    }, 10)
}, function (err, mappedArray) {
    // all done, continue here
    console.log(mappedArray)
})
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.