对 Promises 和 async-await 感到困惑

Confused about Promises and async-await

我正在使用 GitHub api 创建一个应用程序,但我在使用异步函数时遇到了问题。我是使用异步的新手,所以非常感谢您的帮助。这是我到目前为止编写的代码:

const getFiles = async function(token, reponame) {
  var gh = new GitHub({
    token: token
  });

  reponame = reponame.split("/");
  const repo = gh.getRepo(reponame[0], reponame[1]);

  let head = new Headers();
  head.append("Authorization: ", "token " + token);

  const getContents = new Promise((res, rej) => {
    repo.getContents(null, "content", true, (err, files) => {
      if (err) rej(err);
      else return files;
    }).then(files => {
      let promises = [
        files.map(
          file =>
            new Promise(res => {
              fetch(file.downloadURL).then(body => {
                res(body.text);
              });
            })
        )
      ];

      const retFiles = [];
      await Promise.all(promises.map(promise => retFiles.push(promise)));
      res(retFiles)
    });
  });

  return getContents;
};

我得到的错误是在我使用 await 的行中意外的保留字。提前致谢

所以我要在这里做一些假设,所以如果我错了请纠正我,我会修正它们。希望这样做,我可以帮助澄清你的理解。

考虑 async/await 的一种简单方法是替换对 .then(callback) 的需求。如果在 async function.

中,我更喜欢使用 await
const getFiles = async function(token, reponame) {
  try {
    var gh = new GitHub({
      token: token
    });
    reponame = reponame.split("/");

    // edit: I removed await from the line below as your original code
    //       did not treat it as a promise
    const repo = gh.getRepo(reponame[0], reponame[1]); 

    // unused code from your post
    let head = new Headers();
    head.append("Authorization: ", "token " + token);

    // the await below assumes that repo.getContents 
    // will return a promise if a callback is not provided
    const files = await repo.getContents(null, "content", true); 

    // updating the code below so that the file requests run in parallel.
    // this means that all requests are going to fire off basically at once
    // as each fetch is called
    const fileRequests = files.map(file => fetch(file.downloadURL))

    // you wont know which failed with the below.
    const results = (await Promise.all(fileRequests)).map(res => res.text)
    return results
  } catch (err) {
    // handle error or..
    throw err;
  }
};

此代码未经测试。我没有使用 github 的 api 所以我最好猜测每个调用在做什么。如果 gh.getReporepo.getContents 没有 return 承诺,则需要进行一些调整。

如果您正在使用的 github 库不会 return 如果未提供回调则承诺:

const getFiles = async function(token, reponame) {
  try {
    var gh = new GitHub({
      token: token
    });
    reponame = reponame.split("/");
    const repo = await gh.getRepo(reponame[0], reponame[1]); 
    let head = new Headers();
    head.append("Authorization: ", "token " + token);

    const getContents = new Promise((res, rej) => {
      repo.getContents(null, "content", true, (err, files) => {
        if (err) { 
          return rej(err);
        }
        res(files)
      })
    })
    const fileRequests = (await getContents).map(file => fetch(file.downloadURL))
    return (await Promise.all(fileRequests)).map(res => res.text)
  } catch (err) {
    // handle error or..
    throw err;
  }
};

这是一个使用 async/await 的示例,它使用新的承诺来承诺回调:

const content = document.getElementById("content")
const result = document.getElementById("result")

async function example(){
  content.innerHTML = 'before promise';
  const getContents = new Promise((res, rej) => {
    setTimeout(()=> {
      res('done')
    }, 1000)
  })
  const res = await getContents
  content.innerHTML = res
  return res
}
example().then((res)=> {
  result.innerHTML = `I finished with <em>${res}</em> as a result`
})
<div id="content"></div>
<div id="result"></div>

这就是为什么我最初用等待每个请求的 for...of 循环编写答案的原因。所有的promise基本上都是一次性执行的:

const content = document.getElementById("content")
const result = document.getElementById("result")
async function example() {
  const promises = []
  content.innerHTML = 'before for loop. each promise updates the content as it finishes.'
  for(let i = 0; i < 20; i++){
    const promise = new Promise((resolve, reject) => {
      setTimeout(()=> {
        content.innerHTML = `current value of i: ${i}`
        resolve(i)
      }, 1000)
    })
    promises.push(promise)
  }
  const results = await Promise.all(promises)
  return results
}

example().then(res => result.innerHTML= res.join(', '))
  content:
  <div id="content"></div>
  
  result:
  <div id="result"></div>

我发现这是修改后的代码:

const getFiles = async function(token, reponame) {
  var gh = new GitHub({
    token: token
  });

  reponame = reponame.split("/");
  const repo = gh.getRepo(reponame[0], reponame[1]);

  let files = new Promise((res, rej) => {
    repo.getContents(null, "content", true, (err, files) => {
      if (err) rej(err);
      else res(files);
    });
  });

  let content = new Promise(res => {
    files.then(files => {
      const promises = files.reduce((result, file) => {
        if (file.name.endsWith(".md")) {
          result.push(
            new Promise((res, rej) => {
              repo.getContents(null, file.path, true, (err, content) => {
                if (err) rej(err);
                else
                  res({
                    path: file.path,
                    content: content
                  });
              });
            })
          );
        }
        return result;
      }, []);

      console.log(promises);

      res(
        Promise.all(
          promises.map(promise =>
            promise.then(file => {
              return file;
            })
          )
        )
      );
    });
  });

  return await content;
};

我仍然不知道这是否是 "right" 的方法,但它确实有效。

await 关键字只能与 async 函数一起使用。如果您注意到,您的 await Promise.all(promises.map(promise => retFiles.push(promise))); 位于在 .then 中传递 files 参数的函数内。只需使该函数 asyncawait 将在作用域内工作。试试下面的代码。

 const getFiles = async function(token, reponame) {
  var gh = new GitHub({
    token: token
  });

  reponame = reponame.split("/");
  const repo = gh.getRepo(reponame[0], reponame[1]);

  let head = new Headers();
  head.append("Authorization: ", "token " + token);

  const getContents = new Promise((res, rej) => {
    repo.getContents(null, "content", true, (err, files) => {
      if (err) rej(err);
      else return files;
    }).then( async (files) => {
      let promises = [
        files.map(
          file =>
            new Promise(res => {
              fetch(file.downloadURL).then(body => {
                res(body.text);
              });
            })
        )
      ];

      const retFiles = [];
      await Promise.all(promises.map(promise => retFiles.push(promise)));
      res(retFiles)
    });
  });

  return getContents;
};

这里有很多问题。代码很复杂;不需要所有这些承诺以及间接层和嵌套来实现您的需要。

您尝试做的模式很常见:

  • 请求获取实体列表(文件、用户、URL...)。
  • 对于列表中 return 的每个实体,再次请求获取更多信息。
  • Return 结果作为承诺(必须是承诺,因为 async 函数只能 return 承诺)。

做到这一点的方法是将问题分成几个阶段。在大多数情况下,使用 awaitasync 关键字而不是 .then。为了使示例可重现,我将使用一个场景,我们希望获取在 GitHub 上创建的最新 n 要点的用户配置文件——这基本上等同于您正在做的事情,我留给你去推断。

第一步是获取实体的初始列表(最近创建的要点):

const res = await fetch("https://api.github.com/gists/public");
const gists = await res.json();

接下来,对于来自 0..n 的要点数组中的每个要点,我们需要触发一个请求。重要的是要确保我们没有使用 await:

在此处序列化任何内容
const requests = gists.slice(0, n).map(gist =>
  fetch(`https://api.github.com/users/${gist.owner.login}`)
);

现在所有请求都在进行中,我们需要等到它们完成。这就是 Promise.all 的用武之地:

const responses = await Promise.all(requests);

最后一步是从每个响应中获取 JSON,这需要另一个 Promise.all:

return await Promise.all(responses.map(e => e.json()));

这是我们的最终结果,可以 returned。这是代码:

const getRecentGistCreators = async (n=1) => {
  try {
    const res = await fetch("https://api.github.com/gists/public");
    const gists = await res.json();
    const requests = gists.slice(0, n).map(gist =>
      fetch(`https://api.github.com/users/${gist.owner.login}`)
    );
    const responses = await Promise.all(requests);
    return await Promise.all(responses.map(e => e.json()));
  }
  catch (err) {
    throw err;
  }
};

(async () => {
  try {
    for (const user of await getRecentGistCreators(5)) {
      const elem = document.createElement("div");
      elem.textContent = user.name;
      document.body.appendChild(elem);
    }
  }
  catch (err) {
    throw err;
  }
})();

作为改进说明,最好在使用额外承诺完成请求的同一阶段请求 JSON,但为了简单起见,我们将分两个连续步骤进行.作为设计要点,将负担过重的 getRecentGistCreators 也分成两个单独的步骤可能会很好。