异步/等待未等待

14

我遇到了一个问题,我并不完全理解。我觉得可能有一些概念我没有掌握,代码可能需要优化,还可能有一些 bug 造成了影响。

为了大大简化整个流程:

  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;
    }
}

注意,我正在使用asyncawaitPromise,相信它们的预期用法。实际发生的情况:所引用数据的调用(获取_scan的结果)最终发生在原始请求返回之后。这最终会向内容模板提供不完整的数据。

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

  • npm@2.14.2
  • node@4.0.0
  • webpack@1.12.2
  • babel@5.8.34
  • babel-loader@5.4.0

你为什么要混合使用 Promise 和 async/await:return new Promise(async (resolve, reject) => { ... }?难道不应该是 async _get(type, id) { ... } 而且没有 Promise 吗? - Shanoor
1个回答

22
我认为问题出在您的_scan函数中的forEach调用上。请参考使用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 的迭代完成,承诺就会被解决,_scan 返回,即使通过 forEach 调用的其他异步函数仍在执行。
如果你想要同时执行它们并在全部完成后调用某些内容,你需要将 forEach 更改为 map 以返回一个承诺数组,然后可以从 _scanawait* 它们(如果你想要同时执行它们并在全部完成后调用某些内容),或者如果你想让它们按顺序执行,则一个接一个地执行它们。
另外,如果我理解正确,你的一些异步函数可以简化一下;请记住,虽然等待 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;
  }
}

这非常有帮助。谢谢。只是一个小提示,你意外地删掉了var val = data.fields[key]; - ArrayKnight
@ArrayKnight 很高兴能帮到你!修正了笔误,谢谢 ^_^ - Michelle Tilley
我从这个地狱中走出来最重要的部分是:“教训是:当你在异步函数中有任何函数时要小心。await 只会暂停其父函数,因此请检查它是否正在执行你实际认为它正在执行的操作。”谢谢!@MichelleTilley - srinivas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接