Async/Await 还没等

Async/Await not waiting

我 运行 遇到了一个我不完全理解的问题。我觉得可能有一些我没有掌握的概念,可以优化的代码,并且可能是为了更好的措施而抛出的错误。

为了大大简化整体流程:

  1. 向外部发出请求API
  2. 返回的 JSON 对象被解析并扫描 link 引用
  3. 如果找到任何 link 引用,将对 populate/replace link 引用和真实 JSON 数据
  4. 发出额外请求
  5. 一旦所有 link 引用都被替换,原始请求将被返回并用于构建内容

这是原始请求 (#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);
        }
    });
}

如果找到 link 引用,则会发出额外的请求,然后将生成的 JSON 注入到原始 JSON 中以代替引用:

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

注意,我使用的是 asyncawaitPromise,我相信他们的意图。 最终发生了什么: 对引用数据的调用(获取 _scan 的结果)最终发生在返回原始请求之后。这最终会向内容模板提供不完整的数据。

有关我的构建设置的其他信息:

我认为问题出在您在 _scan 中的 forEach 电话中。作为参考,请参阅 Taming the asynchronous beast with ES7:

中的这段话

However, if you try to use an async function, then you will get a more subtle bug:

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');

This will compile, but the problem is that this will print out:

main loop done
0
1
2

What's happening is that the main function is exiting early, because the await is actually in the sub-function. Furthermore, this will execute each promise concurrently, which is not what we intended.

The lesson is: be careful when you have any function inside your async function. The await will only pause its parent function, so check that it's doing what you actually think it's doing.

因此 forEach 调用的每次迭代都是 运行 并发的;他们不是一次执行一个。一旦符合条件 i === keys.length - 1 的函数完成,promise 就会被解决并且 _scan returns,即使通过 forEach 调用的其他异步函数仍在执行。

您需要将 forEach 更改为 map 到 return 一组承诺,然后您可以从 _scan await* (如果你想同时执行它们,然后在它们全部完成时调用一些东西),或者如果你希望它们按顺序执行,则一次执行一个。


附带说明一下,如果我没看错的话,您的一些异步函数可以稍微简化;请记住,虽然 awaiting 一个 async 函数调用 return 是一个值,但简单地称它为 return 另一个承诺,并且 returning 一个来自 async 函数与 return 在非 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;
  }
}