Async / Await не ждет

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

Чтобы значительно упростить общий поток:

  1. Сделан запрос к внешнему API
  2. Возвращенный объект JSON анализируется и сканируется на предмет ссылок на ссылки.
  3. Если найдены какие-либо ссылки на ссылки, делаются дополнительные запросы для заполнения / замены ссылок ссылок реальными данными JSON.
  4. После замены всех ссылок на ссылки возвращается исходный запрос, который используется для создания контента.

Вот исходный запрос (№1):

await Store.get(Constants.Contentful.ENTRY, Contentful[page.file])

Store.get представлен:

async get(type, id) {
    return await this._get(type, id);
}

Какие звонки:

_get(type, id) {
    return new Promise(async (resolve, reject) => {
        var data = _json[id] = _json[id] || await this._api(type, id);

        console.log(data)

        if(isAsset(data)) {
            resolve(data);
        } else if(isEntry(data)) {
            await this._scan(data);

            resolve(data);
        } else {
            const error = 'Response is not entry/asset.';

            console.log(error);

            reject(error);
        }
    });
}

Вызов API:

_api(type, id) {
    return new Promise((resolve, reject) => {
        Request('http://cdn.contentful.com/spaces/' + Constants.Contentful.SPACE + '/' + (!type || type === Constants.Contentful.ENTRY ? 'entries' : 'assets') + '/' + id + '?access_token=' + Constants.Contentful.PRODUCTION_TOKEN, (error, response, data) => {
            if(error) {
                console.log(error);

                reject(error);
            } else {
                data = JSON.parse(data);

                if(data.sys.type === Constants.Contentful.ERROR) {
                    console.log(data);

                    reject(data);
                } else {
                    resolve(data);
                }
            }
        });
    });
}

Когда запись возвращается, она сканируется:

_scan(data) {
    return new Promise((resolve, reject) => {
        if(data && data.fields) {
            const keys = Object.keys(data.fields);

            keys.forEach(async (key, i) => {
                var val = data.fields[key];

                if(isLink(val)) {
                    var child = await this._get(val.sys.linkType.toUpperCase(), val.sys.id);

                    this._inject(data.fields, key, undefined, child);
                } else if(isLinkArray(val)) {
                    var children = await* val.map(async (link) => await this._get(link.sys.linkType.toUpperCase(), link.sys.id));

                    children.forEach((child, index) => {
                        this._inject(data.fields, key, index, child);
                    });
                } else {
                    await new Promise((resolve) => setTimeout(resolve, 0));
                }

                if(i === keys.length - 1) {
                    resolve();
                }
            });
        } else {
            const error = 'Required data is unavailable.';

            console.log(error);

            reject(error);
        }
    });
}

Если ссылки на ссылки найдены, выполняются дополнительные запросы, а затем полученный JSON вставляется в исходный JSON вместо ссылки:

_inject(fields, key, index, data) {
    if(isNaN(index)) {
        fields[key] = data;
    } else {
        fields[key][index] = data;
    }
}

Заметьте, я использую async, await и Promise, я верю в их предназначение. Что в итоге происходит: вызовы данных, на которые имеются ссылки (получаются в результате _scan), в конечном итоге происходят после того, как исходный запрос был возвращен. Это приводит к тому, что в шаблон контента предоставляются неполные данные.

Дополнительная информация о настройке моей сборки:


person ArrayKnight    schedule 19.12.2015    source источник
comment
Почему вы смешиваете обещания и async / await: return new Promise(async (resolve, reject) => { ... }? Разве это не должно быть async _get(type, id) { ... } и вообще никакого обещания?   -  person Shanoor    schedule 19.12.2015


Ответы (1)


Я считаю, что проблема в вашем forEach звонке в _scan. Для справки см. Этот отрывок в статье Укрощение асинхронного зверя с ES7:

Однако, если вы попытаетесь использовать асинхронную функцию, вы получите более тонкую ошибку:

let docs = [{}, {}, {}];

// WARNING: this won't work
docs.forEach(async function (doc, i) {
  await db.post(doc);
  console.log(i);
});
console.log('main loop done');

Это будет скомпилировано, но проблема в том, что он распечатает:

main loop done
0
1
2

Происходит то, что основная функция выходит раньше, потому что await фактически находится в подфункции. Более того, это будет выполнять каждое обещание одновременно, чего мы не планировали.

Урок такой: будьте осторожны, когда у вас есть какая-либо функция внутри асинхронной функции. await только приостанавливает свою родительскую функцию, поэтому убедитесь, что она делает то, что вы на самом деле думаете.

Таким образом, каждая итерация вызова forEach выполняется одновременно; они не выполняются по одному. Как только тот, который соответствует критериям i === keys.length - 1, завершается, обещание разрешается и _scan возвращается, даже если другие асинхронные функции, вызываемые через forEach, все еще выполняются.

Вам нужно будет либо изменить forEach на map, чтобы вернуть массив обещаний, которые затем можно await* из _scan (если вы хотите выполнить их все одновременно, а затем вызвать что-то, когда все они будут выполнены), либо выполнить их одно -at-a-time, если вы хотите, чтобы они выполнялись последовательно.


В качестве примечания: если я правильно их читаю, некоторые из ваших асинхронных функций можно немного упростить; помните, что, хотя await выполнение вызова функции async возвращает значение, простой вызов функции возвращает другое обещание, а возвращение значения из функции async аналогично возврату обещания, которое разрешается к этому значению в функции, отличной от async. Так, например, _get может быть:

async _get(type, id) {
  var data = _json[id] = _json[id] || await this._api(type, id);

  console.log(data)

  if (isAsset(data)) {
    return data;
  } else if (isEntry(data)) {
    await this._scan(data);
    return data;
  } else {
    const error = 'Response is not entry/asset.';
    console.log(error);
    throw error;
  }
}

Точно так же _scan может быть (при условии, что вы хотите, чтобы тела forEach выполнялись одновременно):

async _scan(data) {
  if (data && data.fields) {
    const keys = Object.keys(data.fields);

    const promises = keys.map(async (key, i) => {
      var val = data.fields[key];

      if (isLink(val)) {
        var child = await this._get(val.sys.linkType.toUpperCase(), val.sys.id);

        this._inject(data.fields, key, undefined, child);
      } else if (isLinkArray(val)) {
        var children = await* val.map(async (link) => await this._get(link.sys.linkType.toUpperCase(), link.sys.id));

        children.forEach((child, index) => {
          this._inject(data.fields, key, index, child);
        });
      } else {
        await new Promise((resolve) => setTimeout(resolve, 0));
      }
    });

    await* promises;
  } else {
    const error = 'Required data is unavailable.';
    console.log(error);
    throw error;
  }
}
person Michelle Tilley    schedule 19.12.2015
comment
Это было очень полезно. Спасибо. Всего одна маленькая записка, вы случайно вырезали var val = data.fields[key]; - person ArrayKnight; 19.12.2015
comment
@ArrayKnight Рад, что это было полезно! Исправлена ​​опечатка, спасибо ^ _ ^ - person Michelle Tilley; 19.12.2015
comment
Самым важным для меня, чтобы выбраться из этого ада, был урок: будьте осторожны, когда у вас есть какая-либо функция внутри вашей асинхронной функции. Await только приостанавливает свою родительскую функцию, поэтому убедитесь, что она делает то, что вы на самом деле думаете. Спасибо! @MichelleTilley - person srinivas; 06.05.2020