最近在看Google Web Fundamental上的這篇複習Promise時,發現一個自己沒怎麼想過的用法:用Array的reduce method串接Promise。

這邊假設一個情境:我現在想透過GitHub API將自己幾個repo的commit紀錄撈回來,parse之後在畫面上照順序顯示repo的名稱、此repo所有commit紀錄的hash與message,使用上述的reduce method將所有promise requests串接在一起:

HTML

<section id="commit-history"></section>

JS

let myRepos = ['baskeeper', 'exe-andyteki', 'git-memo'];

function getJSON(url) {
  return fetch(url)
  .then(function(response) {
    return response.json()
  }).then(function(json) {
    return json
  }).catch(function(ex) {
    throw ex;
  })
}

function setCommitHistoryToUI(repoName, repoCommits) {
  let domCommitHistoryBlock = document.getElementById('commit-history');
  
  let domTitle = document.createElement('h3');  
  domTitle.textContent = repoName;
  domCommitHistoryBlock.appendChild(domTitle);
  
  repoCommits.forEach((commit) => {
    let sha = commit.sha.substr(0, 7);
    let message = commit.commit.message;
    let domCommitHistory = document.createElement('p');
    
    domCommitHistory.textContent = `${sha}: ${message}`;
    domCommitHistoryBlock.appendChild(domCommitHistory);
  });
}

// 一開始使用一個直接resolve的promise當作reduce的起始值方便
myRepos.reduce((finishedRequests, repoName) => {
  return finishedRequests
  	.then(() => getJSON(`https://api.github.com/repos/andy02081224/${repoName}/commits`))
  	.then((repoCommits) => setCommitHistoryToUI(repoName, repoCommits))
}, Promise.resolve());

這時候會面臨到一個問題:我的requests是依序打出去的,也就是說在第一個請求完成之後才會接著發出第二個請求,接著是第三個,以此類推,浪費了瀏覽器提供的可以打parellel requests的特性,這一次先將所有請求發出,再利用Promise.all method來等待所有請求完成:

let allRepoCommitsRequests = myRepos.map((repoName) => {   
  return getJSON(`https://api.github.com/repos/andy02081224/${repoName}/commits`);
});

Promise.all(allRepoCommitsRequests)
    .then((allRepoCommits) => {
      allRepoCommits.forEach((repoCommits, i) => {
        setCommitHistoryToUI(myRepos[i], repoCommits)
      });
});

但是,又有一種狀況是,我們希望可以同時發送請求,並在結果回來時依照順序顯示。

現在雖然可以同時發送請求,但也因為Promise.all的關係,需要等到所有請求都完成後才處理資料並把他設定到畫面上,為了達成前述的scenario,以下先把所有請求發送出去後,在依序把它串接起來:

let allRepoCommitsRequests = myRepos.map((repoName) => {   
  return getJSON(`https://api.github.com/repos/andy02081224/${repoName}/commits`);
});

allRepoCommitsRequests.reduce((finishedRequests, request) => {
  return finishedRequests
  	.then(() => request)
  	.then((repoCommits) => setCommitHistoryToUI(myRepos[i], repoCommits))
}, Promise.resolve());

以上三個範例的推進完全是仿照一開始提到的文章,這邊僅是改寫成我自己的例子;個人的理解是,他們之間不是由好到不好的層級差別,而是case by case的應用,端看現在的scenario適合哪個。