异步/等待不等待

时间:2015-12-19 00:30:01

标签: node.js reactjs webpack babeljs ecmascript-next

我遇到了一个我不完全理解的问题。我觉得有可能是我没有掌握的概念,可以优化的代码,以及可能出现的错误。

大大简化整体流程:

  1. 向外部API发出请求
  2. 解析并扫描返回的JSON对象以获取链接引用
  3. 如果找到任何链接引用,则会使用真实JSON数据填充/替换链接引用的其他请求
  4. 更换所有链接引用后,将返回原始请求并用于构建内容
  5. 这里是原始请求(#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

1 个答案:

答案 0 :(得分:13)

我认为问题出在forEach _scan来电。供参考,请参阅Taming the asynchronous beast with 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(如果您要执行)它们全部同时发生,然后在它们全部完成时调用它们,或者如果你希望它们按顺序执行,则一次执行一次。

作为旁注,如果我正确读取它们,可以简化一些异步功能;请记住,await async函数调用返回一个值,只需调用它就会返回另一个promise,而从async函数返回一个值与返回一个解析的promise相同非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;
  }
}