Gọi lại sau khi tất cả các cuộc gọi lại không đồng bộ forEach được hoàn thành


245

Như tiêu đề cho thấy. Làm thế nào để tôi làm điều này?

Tôi muốn gọi whenAllDone()sau khi vòng lặp forEach đã đi qua từng phần tử và thực hiện một số xử lý không đồng bộ.

[1, 2, 3].forEach(
  function(item, index, array, done) {
     asyncFunction(item, function itemDone() {
       console.log(item + " done");
       done();
     });
  }, function allDone() {
     console.log("All done");
     whenAllDone();
  }
);

Có thể làm cho nó hoạt động như thế này? Khi đối số thứ hai cho forEach là một hàm gọi lại chạy khi nó đã trải qua tất cả các lần lặp?

Sản lượng dự kiến:

3 done
1 done
2 done
All done!

13
Sẽ thật tuyệt nếu forEachphương thức mảng tiêu chuẩn có donetham số allDonegọi lại và gọi lại!
Vanuan

22
Thật xấu hổ khi một cái gì đó quá đơn giản lại đòi hỏi quá nhiều vật lộn trong JavaScript.
Ali

Câu trả lời:


410

Array.forEach không cung cấp sự độc đáo này (ồ nếu có) nhưng có một số cách để thực hiện những gì bạn muốn:

Sử dụng một bộ đếm đơn giản

function callback () { console.log('all done'); }

var itemsProcessed = 0;

[1, 2, 3].forEach((item, index, array) => {
  asyncFunction(item, () => {
    itemsProcessed++;
    if(itemsProcessed === array.length) {
      callback();
    }
  });
});

(cảm ơn @vanuan và những người khác) Cách tiếp cận này đảm bảo rằng tất cả các mục được xử lý trước khi gọi lại cuộc gọi lại "đã hoàn thành". Bạn cần sử dụng một bộ đếm được cập nhật trong cuộc gọi lại. Tùy thuộc vào giá trị của tham số chỉ mục không cung cấp cùng một bảo đảm, bởi vì thứ tự trả về của các hoạt động không đồng bộ không được đảm bảo.

Sử dụng lời hứa ES6

(một thư viện lời hứa có thể được sử dụng cho các trình duyệt cũ hơn):

  1. Xử lý tất cả các yêu cầu đảm bảo thực hiện đồng bộ (ví dụ 1 rồi 2 rồi 3)

    function asyncFunction (item, cb) {
      setTimeout(() => {
        console.log('done with', item);
        cb();
      }, 100);
    }
    
    let requests = [1, 2, 3].reduce((promiseChain, item) => {
        return promiseChain.then(() => new Promise((resolve) => {
          asyncFunction(item, resolve);
        }));
    }, Promise.resolve());
    
    requests.then(() => console.log('done'))
  2. Xử lý tất cả các yêu cầu không đồng bộ mà không thực hiện "đồng bộ" (2 có thể kết thúc nhanh hơn 1)

    let requests = [1,2,3].map((item) => {
        return new Promise((resolve) => {
          asyncFunction(item, resolve);
        });
    })
    
    Promise.all(requests).then(() => console.log('done'));

Sử dụng thư viện không đồng bộ

Có thư viện không đồng bộ khác, async là phổ biến nhất, đó là cung cấp cơ chế để thể hiện những gì bạn muốn.

Biên tập

Phần chính của câu hỏi đã được chỉnh sửa để xóa mã ví dụ đồng bộ trước đó, vì vậy tôi đã cập nhật câu trả lời của mình để làm rõ. Ví dụ ban đầu được sử dụng đồng bộ như mã để mô hình hóa hành vi không đồng bộ, do đó, áp dụng như sau:

array.forEachđồng bộ và cũng vậy res.write, vì vậy bạn chỉ cần đặt lại cuộc gọi của mình sau cuộc gọi của bạn để báo trước:

  posts.foreach(function(v, i) {
    res.write(v + ". index " + i);
  });

  res.end();

31
Tuy nhiên, lưu ý rằng nếu có nội dung không đồng bộ bên trong forEach (ví dụ: bạn đang lặp qua một loạt các URL và thực hiện HTTP GET trên chúng), không có gì đảm bảo rằng res.end sẽ được gọi sau cùng.
AlexMA

Để thực hiện gọi lại sau khi hành động không đồng bộ được thực hiện trong một vòng lặp, bạn có thể sử dụng từng phương thức của tiện ích async: github.com/caolan/async#each
elkelk

2
@Vanuan tôi đã cập nhật câu trả lời của mình để phù hợp hơn với chỉnh sửa khá quan trọng của bạn :)
Nick Tomlin

4
tại sao không chỉ if(index === array.length - 1)và loại bỏitemsProcessed
Amin Jafari

5
@AminJafari vì các cuộc gọi không đồng bộ có thể không giải quyết theo thứ tự chính xác mà chúng đã được đăng ký (giả sử bạn đang gọi đến một máy chủ và nó bị chậm một chút trong cuộc gọi thứ 2 nhưng xử lý cuộc gọi cuối cùng tốt). Cuộc gọi không đồng bộ cuối cùng có thể giải quyết trước các cuộc gọi trước. Đột biến một nhân viên bảo vệ chống lại điều này vì tất cả các cuộc gọi lại phải bắn bất kể thứ tự mà họ giải quyết.
Nick Tomlin

25

Nếu bạn gặp phải các hàm không đồng bộ và bạn muốn đảm bảo rằng trước khi thực thi mã, nó sẽ hoàn thành nhiệm vụ, chúng ta luôn có thể sử dụng khả năng gọi lại.

Ví dụ:

var ctr = 0;
posts.forEach(function(element, index, array){
    asynchronous(function(data){
         ctr++; 
         if (ctr === array.length) {
             functionAfterForEach();
         }
    })
});

Lưu ý: functionAfterForEachlà chức năng sẽ được thực hiện sau khi nhiệm vụ foreach kết thúc. asynchronouslà hàm không đồng bộ được thực thi bên trong foreach.


9
Điều này sẽ không hoạt động vì thứ tự thực hiện các yêu cầu không đồng bộ không được bảo đảm. Yêu cầu async cuối cùng có thể kết thúc trước các yêu cầu khác và thực thi hàmAfterForEach () trước khi tất cả các yêu cầu được thực hiện.
Rémy DAVID

@ RémyDAVID yep bạn có một điểm liên quan đến thứ tự thực hiện hoặc tôi sẽ nói quá trình này kết thúc bao lâu, javascript được xử lý đơn để cuối cùng nó hoạt động. Và bằng chứng là upvote câu trả lời này nhận được.
Emil Reña Enriquez

1
Tôi không chắc tại sao bạn có quá nhiều upvote, nhưng Rémi là chính xác. Mã của bạn hoàn toàn không hoạt động vì không đồng bộ có nghĩa là bất kỳ yêu cầu nào cũng có thể trở lại bất cứ lúc nào. Mặc dù JavaScript không phải là đa luồng, trình duyệt của bạn là. Nặng hơn, tôi có thể thêm. Do đó, nó có thể gọi bất kỳ một trong số các cuộc gọi lại của bạn bất cứ lúc nào theo bất kỳ thứ tự nào tùy thuộc vào thời điểm nhận được trả lời từ máy chủ ...
Alexis Wilke

2
yeah, đây là câu trả lời là hoàn toàn sai. Nếu tôi chạy song song 10 lần tải xuống, tất cả đều đảm bảo rằng lần tải xuống cuối cùng kết thúc trước phần còn lại và do đó chấm dứt thực hiện.
knrdk

Tôi sẽ đề nghị bạn sử dụng bộ đếm để tăng số lượng tác vụ không đồng bộ đã hoàn thành và khớp với độ dài của mảng thay vì chỉ mục. Số lượng upvote không liên quan gì đến bằng chứng về tính chính xác của câu trả lời.
Alex

17

Hy vọng điều này sẽ khắc phục vấn đề của bạn, tôi thường làm việc với điều này khi tôi cần thực thi forEach với các tác vụ không đồng bộ bên trong.

foo = [a,b,c,d];
waiting = foo.length;
foo.forEach(function(entry){
      doAsynchronousFunction(entry,finish) //call finish after each entry
}
function finish(){
      waiting--;
      if (waiting==0) {
          //do your Job intended to be done after forEach is completed
      } 
}

với

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}

Tôi đã gặp một vấn đề tương tự trong mã Angular 9 của mình và câu trả lời này đã giúp tôi rất nhiều. Mặc dù câu trả lời @Emil Reña Enriquez cũng có tác dụng với tôi nhưng tôi thấy đây là câu trả lời chính xác và đơn giản hơn cho vấn đề này.
omostan

17

Thật kỳ lạ khi có bao nhiêu câu trả lời không chính xác đã được đưa ra cho trường hợp không đồng bộ ! Nó có thể được chỉ ra một cách đơn giản rằng chỉ số kiểm tra không cung cấp hành vi dự kiến:

// INCORRECT
var list = [4000, 2000];
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
    }, l);
});

đầu ra:

4000 started
2000 started
1: 2000
0: 4000

Nếu chúng tôi kiểm tra index === array.length - 1, gọi lại sẽ được gọi khi hoàn thành lần lặp đầu tiên, trong khi phần tử đầu tiên vẫn đang chờ xử lý!

Để giải quyết vấn đề này mà không cần sử dụng các thư viện bên ngoài như async, tôi nghĩ rằng cách tốt nhất của bạn là lưu chiều dài của danh sách và giảm dần nếu sau mỗi lần lặp. Vì chỉ có một chủ đề, chúng tôi chắc chắn không có cơ hội điều kiện cuộc đua.

var list = [4000, 2000];
var counter = list.length;
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
        counter -= 1;
        if ( counter === 0)
            // call your callback here
    }, l);
});

1
Đó có lẽ là giải pháp duy nhất. Có thư viện async cũng sử dụng quầy?
Vanuan

1
Mặc dù các giải pháp khác thực hiện công việc, nhưng điều này hấp dẫn nhất bởi vì nó không đòi hỏi phải xâu chuỗi hoặc thêm phức tạp. KISS
azatar

Ngoài ra, hãy xem xét tình huống khi độ dài mảng bằng 0, trong trường hợp này, cuộc gọi lại sẽ không bao giờ được gọi
Saeed Ir

6

Với ES2018, bạn có thể sử dụng các trình vòng lặp async:

const asyncFunction = a => fetch(a);
const itemDone = a => console.log(a);

async function example() {
  const arrayOfFetchPromises = [1, 2, 3].map(asyncFunction);

  for await (const item of arrayOfFetchPromises) {
    itemDone(item);
  }

  console.log('All done');
}

1
Có sẵn trong Node v10
Matt Swezey

2

Giải pháp của tôi không có Promise (điều này đảm bảo rằng mọi hành động đều kết thúc trước khi hành động tiếp theo bắt đầu):

Array.prototype.forEachAsync = function (callback, end) {
        var self = this;
    
        function task(index) {
            var x = self[index];
            if (index >= self.length) {
                end()
            }
            else {
                callback(self[index], index, self, function () {
                    task(index + 1);
                });
            }
        }
    
        task(0);
    };
    
    
    var i = 0;
    var myArray = Array.apply(null, Array(10)).map(function(item) { return i++; });
    console.log(JSON.stringify(myArray));
    myArray.forEachAsync(function(item, index, arr, next){
      setTimeout(function(){
        $(".toto").append("<div>item index " + item + " done</div>");
        console.log("action " + item + " done");
        next();
      }, 300);
    }, function(){
        $(".toto").append("<div>ALL ACTIONS ARE DONE</div>");
        console.log("ALL ACTIONS ARE DONE");
    });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="toto">

</div>


1
 var counter = 0;
 var listArray = [0, 1, 2, 3, 4];
 function callBack() {
     if (listArray.length === counter) {
         console.log('All Done')
     }
 };
 listArray.forEach(function(element){
     console.log(element);
     counter = counter + 1;
     callBack();
 });

1
Nó sẽ không hoạt động bởi vì nếu bạn sẽ có hoạt động async bên trong foreach.
Bò tót Sudhanshu


0

Giải pháp của tôi:

//Object forEachDone

Object.defineProperty(Array.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var counter = 0;
        this.forEach(function(item, index, array){
            task(item, index, array);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});


//Array forEachDone

Object.defineProperty(Object.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var obj = this;
        var counter = 0;
        Object.keys(obj).forEach(function(key, index, array){
            task(obj[key], key, obj);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});

Thí dụ:

var arr = ['a', 'b', 'c'];

arr.forEachDone(function(item){
    console.log(item);
}, function(){
   console.log('done');
});

// out: a b c done

Giải pháp rất sáng tạo nhưng sắp có lỗi - "nhiệm vụ không phải là chức năng"
Thiên tài

0

Tôi thử Easy Way để giải quyết nó, chia sẻ với bạn:

let counter = 0;
            arr.forEach(async (item, index) => {
                await request.query(item, (err, recordset) => {
                    if (err) console.log(err);

                    //do Somthings

                    counter++;
                    if(counter == tableCmd.length){
                        sql.close();
                        callback();
                    }
                });

requestlà Chức năng của Thư viện mssql trong Node js. Điều này có thể thay thế từng chức năng hoặc Mã bạn muốn. Chúc may mắn


0
var i=0;
const waitFor = (ms) => 
{ 
  new Promise((r) => 
  {
   setTimeout(function () {
   console.log('timeout completed: ',ms,' : ',i); 
     i++;
     if(i==data.length){
      console.log('Done')  
    }
  }, ms); 
 })
}
var data=[1000, 200, 500];
data.forEach((num) => {
  waitFor(num)
})

-2

Bạn không cần gọi lại để lặp qua danh sách. Chỉ cần thêm end()cuộc gọi sau vòng lặp.

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();

3
Không. OP nhấn mạnh rằng logic không đồng bộ sẽ thực thi cho mỗi lần lặp. res.writeKHÔNG phải là một hoạt động không đồng bộ, vì vậy mã của bạn sẽ không hoạt động.
Jim G.

-2

Một giải pháp đơn giản sẽ như sau

function callback(){console.log("i am done");}

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}

3
Không hoạt động đối với mã không đồng bộ, đó là toàn bộ tiền đề của câu hỏi.
grg

-3

Làm thế nào về setInterval, để kiểm tra số lần lặp hoàn chỉnh, mang lại sự đảm bảo. không chắc chắn nếu nó không quá tải phạm vi nhưng tôi sử dụng nó và dường như là một trong những

_.forEach(actual_JSON, function (key, value) {

     // run any action and push with each iteration 

     array.push(response.id)

});


setInterval(function(){

    if(array.length > 300) {

        callback()

    }

}, 100);

Điều này có vẻ đơn giản
Zeal Murapa
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.