Добавление обещания в Promise.all()

У меня есть вызов API, который иногда возвращает выгруженные ответы. Я хотел бы автоматически добавить их к своим обещаниям, чтобы получить обратный вызов после получения всех данных.

Это моя попытка. Я ожидаю, что новое обещание будет добавлено, а Promise.all разрешится, как только это будет сделано.

На самом деле происходит то, что Promise.all не ждет второго запроса. Я предполагаю, что Promise.all подключает "слушателей" при вызове.

Есть ли способ «повторно инициализировать» Promise.all()?

function testCase (urls, callback) {
    var promises = [];
    $.each(urls, function (k, v) {
        promises.push(new Promise(function(resolve, reject) {
            $.get(v, function(response) {
                if (response.meta && response.meta.next) {
                    promises.push(new Promise(function (resolve, reject) {
                        $.get(v + '&offset=' + response.meta.next, function (response) {
                            resolve(response);
                        });
                    }));
                }
                resolve(response);
            }).fail(function(e) {reject(e)});
        }));
    });

    Promise.all(promises).then(function (data) {
        var response = {resource: []};
        $.each(data, function (i, v) {
            response.resource = response.resource.concat(v.resource);
        });
        callback(response);
    }).catch(function (e) {
        console.log(e);
    });
}   

Желаемый поток выглядит примерно так:

  1. Создайте набор обещаний.
  2. Некоторые обещания порождают новые обещания.
  3. Как только все исходные промисы и порожденные промисы разрешены, вызовите обратный вызов.

person Josiah    schedule 13.02.2017    source источник
comment
Где вы инициализируете promises? Я вижу, как вы подталкиваете к этому, но я не вижу, как вы это создаете.   -  person T.J. Crowder    schedule 13.02.2017
comment
Что такое url? (Если это массив, обычно это множественное число, например, urls.)   -  person T.J. Crowder    schedule 13.02.2017
comment
Почему response.resource = response.resource.concat(v.resource);? Это создает новый массив каждый раз...?   -  person T.J. Crowder    schedule 13.02.2017
comment
@ T.J.Crowder - спасибо за уловы. Я немного подчистил тестовый пример. Это не мой производственный код, просто демонстрирует проблему.   -  person Josiah    schedule 13.02.2017
comment
Если в ответе есть response.meta.next, хотите ли вы, чтобы и исходный ответ, и следующий ответ были в результате?   -  person T.J. Crowder    schedule 13.02.2017
comment
Да, я хочу, чтобы оба ответа были одним большим ответом. Вот почему я объединяю два (или более) массива.   -  person Josiah    schedule 13.02.2017
comment
Но это потом и по ответам на urls, а не по результату response.meta.next.   -  person T.J. Crowder    schedule 13.02.2017
comment
@T.J.Crowder Моя цель — обработать промисы для response.meta.next и URL-адреса в одном и том же распознавателе промисов. Вот почему я пытаюсь добавить обещание к Promises.all.   -  person Josiah    schedule 13.02.2017
comment
Кажется, это дубликат этого вопроса: Как узнать, когда все обещания разрешены в динамическом «итерируемом» параметре? . Маркировка как обман; Я поместил одно потенциальное решение в мой ответ здесь.   -  person Jeff Bowman    schedule 13.02.2017
comment
@JeffBowman - кажется, это то же самое. Хотя мне больше нравится мой титул.   -  person Josiah    schedule 13.02.2017
comment
Мне твой тоже больше нравится; цель состоит просто в том, чтобы оба вопроса указывали на каноническую серию ответов, полезных для обоих. Пометка как обман по-прежнему позволяет проголосовать за ваш вопрос.   -  person Jeff Bowman    schedule 13.02.2017


Ответы (2)


Похоже, что общая цель такова:

  1. For each entry in urls, call $.get and wait for it to complete.
    • If it returns just a response without "next", keep that one response
    • Если он возвращает ответ со следующим, мы также хотим запросить следующий, а затем сохранить их оба.
  2. Вызовите обратный вызов с помощью response, когда вся работа будет выполнена.

Я бы изменил № 2, чтобы вы просто вернули обещание и выполнили его с помощью response.

Ключевым моментом промисов является то, что then возвращает новое обещание, которое будет разрешено на основе того, что вы вернете: если вы вернете значение, которое нельзя изменить, обещание будет выполнено с этим значением; если вы возвращаете thenable, обещание разрешается возвращаемому вами thenable. Это означает, что если у вас есть источник обещаний (в данном случае $.get), вам почти никогда не понадобится использовать new Promise; просто используйте обещания, которые вы создаете с помощью then. (И catch.)

(Если термин thenable вам не знаком или вы не понимаете разницу между выполнением и разрешением, я расскажу о терминологии обещаний в этот пост в моем блоге.)

Смотрите комментарии:

function testCase(urls) {
    // Return a promise that will be settled when the various `$.get` calls are
    // done.
    return Promise.all(urls.map(function(url) {
        // Return a promise for this `$.get`.
        return $.get(url)
            .then(function(response) {
                if (response.meta && response.meta.next) {
                    // This `$.get` has a "next", so return a promise waiting
                    // for the "next" which we ultimately fulfill (via `return`)
                    // with an array with both the original response and the
                    // "next". Note that by returning a thenable, we resolve the
                    // promise created by `then` to the thenable we return.
                    return $.get(url + "&offset=" + response.meta.next)
                        .then(function(nextResponse) {
                            return [response, nextResponse];
                        });
                } else {
                    // This `$.get` didn't have a "next", so resolve this promise
                    // directly (via `return`) with an array (to be consistent
                    // with the above) with just the one response in it. Since
                    // what we're returning isn't thenable, the promise `then`
                    // returns is resolved with it.
                    return [response];
                }
            });
    })).then(function(responses) {
        // `responses` is now an array of arrays, where some of those will be one
        // entry long, and others will be two (original response and next).
        // Flatten it, and return it, which will settle he overall promise with
        // the flattened array.
        var flat = [];
        responses.forEach(function(responseArray) {
            // Push all promises from `responseArray` into `flat`.
            flat.push.apply(flat, responseArray);
        });
        return flat;
    });
}

Обратите внимание, что мы никогда не используем здесь catch; мы откладываем обработку ошибок вызывающей стороне.

Применение:

testCase(["url1", "url2", "etc."])
    .then(function(responses) {
        // Use `responses` here
    })
    .catch(function(error) {
        // Handle error here
    });

Функция testCase выглядит очень длинной, но это только из-за комментариев. Вот без них:

function testCase(urls) {
    return Promise.all(urls.map(function(url) {
        return $.get(url)
            .then(function(response) {
                if (response.meta && response.meta.next) {
                    return $.get(url + "&offset=" + response.meta.next)
                        .then(function(nextResponse) {
                            return [response, nextResponse];
                        });
                } else {
                    return [response];
                }
            });
    })).then(function(responses) {
        var flat = [];
        responses.forEach(function(responseArray) {
            flat.push.apply(flat, responseArray);
        });
        return flat;
    });
}

... и было бы еще проще, если бы мы использовали стрелочные функции ES2015. :-)


В комментарии вы спросили:

Сможет ли это справиться, если будет следующее следующее? Нравится страница 3 результатов?

Мы можем сделать это, инкапсулировав эту логику в функцию, которую мы используем вместо $.get, которую мы можем использовать рекурсивно:

function getToEnd(url, target, offset) {
    // If we don't have a target array to fill in yet, create it
    if (!target) {
        target = [];
    }
    return $.get(url + (offset ? "&offset=" + offset : ""))
        .then(function(response) {
            target.push(response);
            if (response.meta && response.meta.next) {
                // Keep going, recursively
                return getToEnd(url, target, response.meta.next);
            } else {
                // Done, return the target
                return target;
            }
        });
}

Тогда наш основной testCase проще:

function testCase(urls) {
    return Promise.all(urls.map(function(url) {
        return getToEnd(url);
    })).then(function(responses) {
        var flat = [];
        responses.forEach(function(responseArray) {
            flat.push.apply(flat, responseArray);
        });
        return flat;
    });
}
person T.J. Crowder    schedule 13.02.2017
comment
Сможет ли это справиться, если будет следующий next? Нравится страница 3 результатов? - person Josiah; 13.02.2017
comment
@Josiah: могло, да. Нам придется изменить логику. Мы, вероятно, хотели бы иметь функцию, которая обрабатывала бы заданный URL-адрес и возвращала обещание, которое было бы разрешено с массивом ответов всех следующих, и использовала бы его рекурсивно. Вы хотели бы передать ему URL-адрес и смещение или что-то подобное. - person T.J. Crowder; 13.02.2017
comment
@Josiah: Оказывается, это довольно просто сделать, я добавил это в конец ответа. - person T.J. Crowder; 13.02.2017
comment
В моей старой необещающей версии я использовал рекурсивную функцию для получения информации. Я думал, что смогу сделать это, используя обратные вызовы, генерирующие обещания, но я думаю, что это, вероятно, чище. Спасибо большое! - person Josiah; 13.02.2017
comment
@Джосия: Не беспокойтесь! getToEnd является рекурсивным, обратите внимание. :-) - person T.J. Crowder; 14.02.2017

Предполагая, что вы используете jQuery v3+, вы можете использовать обещания, возвращаемые $.ajax, для перехода к Promise.all().

Чего вам не хватает, так это возврата второго запроса в качестве обещания вместо того, чтобы пытаться отправить его в массив обещаний.

Упрощенный пример

var promises = urls.map(function(url) {
  // return promise returned by `$.ajax`
  return $.get(url).then(function(response) {
    if (response.meta) {
      // return a new promise
      return $.get('special-data.json').then(function(innerResponse) {
        // return innerResponse to resolve promise chain
        return innerResponse;
      });

    } else {
      // or resolve with first response
      return response;
    }
  });

})

Promise.all(promises).then(function(data) {
  console.dir(data)
}).catch(function(e) {
  console.log(e);
});

ДЕМО

person charlietfl    schedule 13.02.2017