Node JS Promise.all và forEach


120

Tôi có một cấu trúc giống như mảng hiển thị các phương thức không đồng bộ. Phương thức không đồng bộ gọi trả về các cấu trúc mảng mà lần lượt hiển thị nhiều phương thức không đồng bộ hơn. Tôi đang tạo một đối tượng JSON khác để lưu trữ các giá trị thu được từ cấu trúc này và vì vậy tôi cần phải cẩn thận về việc theo dõi các tham chiếu trong các lệnh gọi lại.

Tôi đã viết mã một giải pháp bạo lực, nhưng tôi muốn tìm hiểu một giải pháp thành ngữ hoặc rõ ràng hơn.

  1. Mẫu phải được lặp lại cho n cấp độ lồng nhau.
  2. Tôi cần sử dụng hứa hẹn.all hoặc một số kỹ thuật tương tự để xác định thời điểm giải quyết quy trình bao vây.
  3. Không phải mọi phần tử đều nhất thiết phải thực hiện một cuộc gọi không đồng bộ. Vì vậy, trong một promise.all lồng nhau, tôi không thể chỉ định các phần tử mảng JSON của mình dựa trên chỉ mục. Tuy nhiên, tôi cần phải sử dụng một cái gì đó như promise.all trong forEach lồng nhau để đảm bảo rằng tất cả các phép gán thuộc tính đã được thực hiện trước khi giải quyết quy trình bao vây.
  4. Tôi đang sử dụng bluebird Prom lib nhưng đây không phải là yêu cầu

Đây là một số mã một phần -

var jsonItems = [];

items.forEach(function(item){

  var jsonItem = {};
  jsonItem.name = item.name;
  item.getThings().then(function(things){
  // or Promise.all(allItemGetThingCalls, function(things){

    things.forEach(function(thing, index){

      jsonItems[index].thingName = thing.name;
      if(thing.type === 'file'){

        thing.getFile().then(function(file){ //or promise.all?

          jsonItems[index].filesize = file.getSize();

Đây là liên kết đến nguồn làm việc mà tôi muốn cải thiện. github.com/pebanfield/change-view-service/blob/master/src/...
user3205931

1
Tôi thấy trong ví dụ bạn đang sử dụng bluebird, bluebird thực sự làm cho cuộc sống của bạn dễ dàng hơn với Promise.map(đồng thời) và Promise.each(tuần tự) trong trường hợp này, cũng lưu ý Promise.deferkhông được dùng nữa - mã trong câu trả lời của tôi cho thấy cách tránh điều đó bằng cách trả lại các lời hứa. Lời hứa là tất cả về giá trị trả về.
Benjamin Gruenbaum

Câu trả lời:


368

Nó khá đơn giản với một số quy tắc đơn giản:

  • Bất cứ khi nào bạn tạo ra một lời hứa trong a then, hãy trả lại nó - bất kỳ lời hứa nào bạn không trả lại sẽ không được chờ đợi ở bên ngoài.
  • Bất cứ khi nào bạn tạo nhiều lời hứa, .allchúng - theo cách đó nó sẽ đợi tất cả các lời hứa và không có lỗi nào từ bất kỳ lời hứa nào trong số chúng sẽ bị tắt tiếng.
  • Bất cứ khi nào bạn tổ thens, bạn thường có thể trở lại ở giữa - thenchuỗi thường nhiều nhất là 1 độ sâu.
  • Bất cứ khi nào bạn thực hiện IO, nó phải là một lời hứa - hoặc nó phải ở trong một lời hứa hoặc nó nên sử dụng một lời hứa để báo hiệu sự hoàn thành của nó.

Và một số mẹo:

  • Ánh xạ được thực hiện tốt .maphơn vớifor/push - nếu bạn đang ánh xạ các giá trị với một hàm, mapcho phép bạn diễn đạt chính xác khái niệm về việc áp dụng từng hành động một và tổng hợp kết quả.
  • Đồng thời tốt hơn so với thực thi tuần tự nếu nó miễn phí - tốt hơn là thực hiện mọi thứ đồng thời và chờ chúng Promise.allhơn là thực hiện từng thứ một - mỗi thứ chờ trước cái tiếp theo.

Được rồi, hãy bắt đầu:

var items = [1, 2, 3, 4, 5];
var fn = function asyncMultiplyBy2(v){ // sample async action
    return new Promise(resolve => setTimeout(() => resolve(v * 2), 100));
};
// map over forEach since it returns

var actions = items.map(fn); // run the function over all items

// we now have a promises array and we want to wait for it

var results = Promise.all(actions); // pass array of promises

results.then(data => // or just .then(console.log)
    console.log(data) // [2, 4, 6, 8, 10]
);

// we can nest this of course, as I said, `then` chains:

var res2 = Promise.all([1, 2, 3, 4, 5].map(fn)).then(
    data => Promise.all(data.map(fn))
).then(function(data){
    // the next `then` is executed after the promise has returned from the previous
    // `then` fulfilled, in this case it's an aggregate promise because of 
    // the `.all` 
    return Promise.all(data.map(fn));
}).then(function(data){
    // just for good measure
    return Promise.all(data.map(fn));
});

// now to get the results:

res2.then(function(data){
    console.log(data); // [16, 32, 48, 64, 80]
});

5
À, một số quy tắc từ quan điểm của bạn :-)
Bergi

1
@Bergi ai đó thực sự nên lập danh sách các quy tắc này và một nền tảng ngắn về lời hứa. Chúng tôi có thể lưu trữ nó tại bluebirdjs.com.
Benjamin Gruenbaum

vì tôi không nên chỉ nói lời cảm ơn - ví dụ này có vẻ tốt và tôi thích gợi ý bản đồ, tuy nhiên, phải làm gì với một tập hợp các đối tượng mà chỉ một số có phương thức không đồng bộ? (Điểm 3 của tôi ở trên) Tôi đã có ý tưởng rằng tôi sẽ trừu tượng hóa logic phân tích cú pháp cho từng phần tử thành một hàm và sau đó để nó giải quyết trên phản hồi cuộc gọi không đồng bộ hoặc nơi không có lệnh gọi không đồng bộ thì nó chỉ cần giải quyết. Điều đó có ý nghĩa?
user3205931 15/07/15

Tôi cũng cần hàm bản đồ trả về cả đối tượng json mà tôi đang xây dựng và kết quả của lệnh gọi không đồng bộ mà tôi cần thực hiện nên tôi cũng không chắc làm thế nào để thực hiện điều đó - cuối cùng toàn bộ điều cần phải được đệ quy vì tôi đang xem một thư mục cấu trúc - tôi vẫn đang nhai về vấn đề này nhưng việc trả tiền là nhận được trong cách :(
user3205931

2
@ user3205931 hứa hẹn đơn giản hơn là dễ dàng - chúng không quen thuộc như những thứ khác nhưng một khi bạn tìm hiểu chúng, chúng sẽ tốt hơn rất nhiều để sử dụng. Hãy kiên nhẫn, bạn sẽ nhận được nó :)
Benjamin Gruenbaum

42

Đây là một ví dụ đơn giản sử dụng giảm. Nó chạy tuần tự, duy trì thứ tự chèn và không yêu cầu Bluebird.

/**
 * 
 * @param items An array of items.
 * @param fn A function that accepts an item from the array and returns a promise.
 * @returns {Promise}
 */
function forEachPromise(items, fn) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item);
        });
    }, Promise.resolve());
}

Và sử dụng nó như thế này:

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

function logItem(item) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            resolve();
        })
    });
}

forEachPromise(items, logItem).then(() => {
    console.log('done');
});

Chúng tôi thấy hữu ích khi gửi một ngữ cảnh tùy chọn vào vòng lặp. Bối cảnh là tùy chọn và được chia sẻ bởi tất cả các lần lặp lại.

function forEachPromise(items, fn, context) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item, context);
        });
    }, Promise.resolve());
}

Hàm hứa của bạn sẽ giống như sau:

function logItem(item, context) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            context.itemCount++;
            resolve();
        })
    });
}

Cảm ơn vì điều này - giải pháp của bạn đã hiệu quả với tôi, nơi những người khác (bao gồm các lib npm khác nhau) không làm được. Bạn đã xuất bản cái này lên npm chưa?
SamF

Cảm ơn bạn. Hàm giả định rằng tất cả các Promise đều được giải quyết. Làm thế nào để chúng ta xử lý những lời hứa bị từ chối? Ngoài ra, làm thế nào để chúng ta xử lý những lời hứa thành công với giá trị?
oyalhi

@oyalhi Tôi khuyên bạn nên sử dụng 'ngữ cảnh' và thêm một mảng các tham số đầu vào bị từ chối được ánh xạ tới lỗi. Điều này thực sự phù hợp với từng trường hợp sử dụng, vì một số sẽ muốn bỏ qua tất cả các lời hứa còn lại và một số thì không. Đối với giá trị trả về, bạn cũng có thể sử dụng cách tiếp cận tương tự.
Steven Spungin

1

Tôi đã trải qua tình huống tương tự. Tôi đã giải quyết bằng cách sử dụng hai Promise.All ().

Tôi nghĩ đó là giải pháp thực sự tốt, vì vậy tôi đã xuất bản nó trên npm: https://www.npmjs.com/package/promise-foreach

Tôi nghĩ mã của bạn sẽ như thế này

var promiseForeach = require('promise-foreach')
var jsonItems = [];
promiseForeach.each(jsonItems,
    [function (jsonItems){
        return new Promise(function(resolve, reject){
            if(jsonItems.type === 'file'){
                jsonItems.getFile().then(function(file){ //or promise.all?
                    resolve(file.getSize())
                })
            }
        })
    }],
    function (result, current) {
        return {
            type: current.type,
            size: jsonItems.result[0]
        }
    },
    function (err, newList) {
        if (err) {
            console.error(err)
            return;
        }
        console.log('new jsonItems : ', newList)
    })

0

Chỉ để thêm vào giải pháp được trình bày, trong trường hợp của tôi, tôi muốn tìm nạp nhiều dữ liệu từ Firebase cho danh sách sản phẩm. Đây là cách tôi đã làm điều đó:

useEffect(() => {
  const fn = p => firebase.firestore().doc(`products/${p.id}`).get();
  const actions = data.occasion.products.map(fn);
  const results = Promise.all(actions);
  results.then(data => {
    const newProducts = [];
    data.forEach(p => {
      newProducts.push({ id: p.id, ...p.data() });
    });
    setProducts(newProducts);
  });
}, [data]);
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.