자바스크립트 프로미스: 소개

프로미스는 지연된 비동기 계산을 단순화합니다. 프로미스는 아직 완료되지 않은 작업을 나타냅니다.

Jake Archibald
Jake Archibald

개발자 여러분, Google Workspace의 역사에서 중요한 순간을 웹 개발에 관한 것입니다.

[드럼 연주]

JavaScript에 프라미스를 사용할 수 있게 되었습니다.

[불꽃이 터지고 반짝이는 종이가 위에서 내리고 관중들은 열광합니다]

이때 사용자는 다음 카테고리 중 하나에 속하게 됩니다.

  • 사람들이 당신 주변에서 환호하고 있지만, 그 호들갑이 뭔지 모르겠어요. 정의합니다. 어떤 '프라미스'가 있습니다. 어깨를 으쓱하지만 반짝이는 종이가 어깨를 짓누릅니다. 그렇다면 내가 이 문제에 관심을 가져야 하는 이유를 이해하는 데 오랜 시간이 걸렸어요. 있습니다. 처음부터 시작하는 것이 좋습니다.
  • 주먹질을 하고 있네요! 때가 됐지? 이전에 사용한 프로미스 항목 모든 구현의 API가 약간 다르다는 점이 성가십니다. 공식 JavaScript 버전을 위한 API는 무엇인가요? 여러분은 아마도 용어를 포함합니다.
  • 여러분은 이미 이것에 대해 알고 있고 뛰어오르고 뉴스처럼 받아들입니다. 잠시 시간을 내어 자신의 우월함을 느끼고 그런 다음 API 참조로 이동하세요.

브라우저 지원 및 폴리필

브라우저 지원

  • Chrome: 32. <ph type="x-smartling-placeholder">
  • Edge: 12. <ph type="x-smartling-placeholder">
  • Firefox: 29. <ph type="x-smartling-placeholder">
  • Safari: 8. <ph type="x-smartling-placeholder">

소스

완전한 프라미스 구현이 없는 브라우저를 사양으로 가져오기 위해 다른 브라우저 및 Node.js에 프라미스를 추가하는 방법은 폴리필을 사용해 (2k gzip으로 압축됨).

무슨 호들갑입니까?

JavaScript는 단일 스레드이므로 두 비트의 스크립트를 할 수 있습니다. 차례로 실행되어야 합니다 브라우저에서 JavaScript는 브라우저마다 다른 많은 다른 것들과 스레드를 공유합니다. 있습니다. 그러나 일반적으로 JavaScript는 페인팅과 동일한 큐에 있으므로 페인팅을 사용자 작업 처리 (예: 텍스트 강조 표시, 상호작용 등) 양식 컨트롤 사용) 이러한 작업 중 하나가 수행되면 다른 작업이 지연됩니다.

인간은 다중 스레드입니다. 여러 손가락으로 입력할 수도 있고 운전을 하면서 동시에 대화를 나눌 수 있습니다. 유일한 차단 방법은 재채기라는 기능을 처리해야 합니다. 재채기를 하는 동안 잠시 멈춰야 합니다 그것은 꽤 성가십니다. 특히 운전하면서 대화를 하려고 할 때 더욱 그렇습니다. 하지 말아야 할 일 재채기 코드를 작성하고자 한다고 가정해 보겠습니다

이 문제를 해결하기 위해 이벤트와 콜백을 사용한 적이 있을 것입니다. 이벤트는 다음과 같습니다.

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

재채기 소리가 아닙니다. 이미지를 가져오고 리스너를 몇 명 추가한 다음 JavaScript는 이러한 리스너 중 하나가 호출될 때까지 실행을 중지할 수 있습니다.

안타깝지만 위 예에서는 오류가 발생할 수 있습니다. 듣기를 시작하지 않으므로 '완전한' 이미지 속성:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

이는 수신하기 전에 오류가 발생한 이미지는 포착하지 않습니다. them; 불행히도 DOM은 그렇게 할 방법을 제공하지 않습니다. 또한 이미지 1개를 로드하는 것입니다. 집합의 값이 언제인지 알고자 한다면 상황이 더욱 복잡해집니다. 개의 이미지가 로드되었습니다.

이벤트가 최선의 방법이 아닐 수 있음

이벤트는 같은 기간에 여러 번 발생할 수 있는 일에 유용합니다. 객체: keyup, touchstart 등. 이러한 이벤트는 별로 중요하지 않습니다. 알 수 있습니다. 그러나 비동기 성공/실패의 경우 다음과 같은 작업을 하는 것이 이상적입니다.

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

이는 프라미스가 하는 일이지만 더 나은 이름을 지정합니다. HTML 이미지 요소에 '준비됨' 메서드가 있으면 다음과 같이 할 수 있습니다.

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

가장 기본적인 프라미스는 다음을 제외하면 이벤트 리스너와 약간 비슷합니다.

  • 프로미스는 한 번만 성공 또는 실패할 수 있습니다. 두 번 성공 또는 실패할 수는 없지만 둘 다 성공에서 실패로 전환하거나 그 반대로 전환할 수 없습니다.
  • 프라미스가 성공 또는 실패한 후 나중에 성공/실패를 추가하는 경우 해당 이벤트에 콜백이 호출된 경우에도 올바른 콜백이 살펴봤습니다

이는 비동기 성공/실패에 매우 유용합니다. 어떤 제품이 출시되었는지 정확히 알고 싶어 하며 영향을 미칠 수 있습니다

프로미스 용어

도메니크 데니콜라의 교정에 대한 초안 읽기 'F'로 채점하고 용어를 정리해 보았습니다 그는 저를 구금했고, 강제로 복사해서 상태와 운명 부모님에게 걱정 편지를 썼습니다. 그럼에도 불구하고 많은 용어가 섞여 있지만 기본 사항은 다음과 같습니다.

프라미스는 다음과 같을 수 있습니다.

  • fulfill - 프로미스와 관련된 작업이 성공했습니다.
  • rejected - 프로미스와 관련된 작업이 실패했습니다.
  • 대기중 - 아직 처리 또는 거부되지 않음
  • 정산됨 - 처리되거나 거부되었습니다.

사양은 또한 thenable이라는 용어를 사용하여 프라미스와 유사한 객체를 설명합니다. 즉, then 메서드가 있습니다. 이 용어를 보면 전 잉글랜드 축구 경기가 생각나네 관리자 테리 베나블스는 최대한 적게 사용하겠습니다.

JavaScript에서 프라미스 도입

프로미스는 한동안 다음과 같은 라이브러리 형식으로 존재했습니다.

위의 프라미스 및 JavaScript 프라미스는 표준화된 공통 동작을 공유합니다. Promises/A+라고 부릅니다. 만약 jQuery 사용자라면 지연. 하지만 Deferred는 Promise/A+를 준수하지 않으므로 미묘하게 다르고 덜 유용함 조심하세요. jQuery에는 프로미스 유형이지만 이 유형은 Deferred의 하위 집합이며 동일한 문제가 있습니다.

약속 구현은 표준화된 동작을 따르지만 전반적인 API가 다릅니다 JavaScript 프로미스는 API에서 RSVP.js와 유사합니다. 프로미스를 만드는 방법은 다음과 같습니다.

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

프라미스 생성자는 한 개의 인수, 두 개의 매개변수가 있는 콜백, 있습니다. 콜백 내에서 비동기 작업 등의 작업을 한 다음 모든 것이 순조롭게 해결되었다면, 그렇지 않으면 거부를 호출합니다.

기존 일반 JavaScript의 throw와 마찬가지로 다음을 수행하는 것이 관례이지만 필수는 아닙니다. 오류를 반환합니다. Error 객체는 오류 객체의 디버깅 도구가 더 유용해집니다.

이 프라미스를 사용하는 방법은 다음과 같습니다.

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then()는 성공 사례의 콜백과 성공 사례의 콜백이라는 두 개의 인수를 사용합니다. 사용합니다 둘 다 선택사항이므로 성공 또는 실패 사례에만 사용할 수 있습니다

JavaScript 프라미스는 DOM에서 'Futures'로 시작하고 'Promises'로 이름을 바꿨습니다. 마지막으로 자바스크립트로 이동했습니다. 이러한 메서드를 DOM은 다음과 같이 브라우저가 아닌 JS 컨텍스트에서 사용할 수 있으므로 유용합니다. Node.js (핵심 API에서 Node.js를 사용하는지 여부는 또 다른 질문입니다.)

비록 자바스크립트 기능이지만 DOM은 그것을 사용하는 것을 두려워하지 않습니다. 포함 사실 비동기 성공/실패 메서드가 있는 모든 새 DOM API는 프로미스를 사용합니다. 이 작업은 이미 할당량 관리 글꼴 로드 이벤트, ServiceWorker 웹 MIDI, 스트림

다른 라이브러리와의 호환성

JavaScript 프라미스 API는 then() 메서드가 있는 모든 것을 다음과 같이 처리합니다. 프라미스 유사 (또는 promise-speak sighthenable)이므로 라이브러리를 사용하는 경우 이는 Q 프로미스를 반환하는 새로운 API인 자바스크립트 프라미스입니다.

그러나 언급했듯이 jQuery의 Deferreds는 다소 유용하지 않습니다. 다행히 표준 프로미스로 캐스팅할 수 있습니다. 가능한 한 빨리:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

여기서 jQuery의 $.ajax는 Deferred를 반환합니다. then() 메서드가 있으므로 Promise.resolve()는 이를 JavaScript 프로미스로 변환할 수 있습니다. 하지만 deferred가 복수의 인수를 콜백에 전달하는 경우도 있습니다. 예를 들면 다음과 같습니다.

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

반면 JavaScript 프라미스는 첫 번째를 제외한 모든 것을 무시합니다.

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

고맙게도 이는 일반적으로 개발자가 원하거나 적어도 검색할 수 있습니다. 또한 jQuery는 오류 객체를 거부로 전달합니다.

복잡한 비동기 코드를 더 쉽게 만들기

그럼, 몇 가지를 코딩해 보겠습니다. 다음을 수행하려고 한다고 가정해 보겠습니다.

  1. 스피너를 시작하여 로드 표시
  2. 스토리의 일부 JSON을 가져와 제목과 각 챕터의 URL을 제공합니다.
  3. 페이지에 제목 추가
  4. 각 챕터 가져오기
  5. 페이지에 스토리 추가
  6. 스피너 중지

... 진행 과정에서 문제가 발생한 경우에도 사용자에게 알립니다. 우리가 스피너를 멈추지 않으면 계속 회전합니다. 다른 UI와 충돌할 수 있습니다.

물론 이야기를 전달하기 위해 JavaScript를 사용하지는 않을 것입니다. 더 빠르게 게재되므로 이 패턴은 API를 다룰 때 매우 일반적입니다. 가져오기만 한 다음, 완료되면 무언가를 합니다.

먼저 네트워크에서 데이터 가져오기를 처리하겠습니다.

XMLHttpRequest 프라미스화

이전 API는 이전 API가 가능한 경우 프로미스를 사용하도록 업데이트됩니다. 사용할 수 있습니다 XMLHttpRequest은(는) 주요 후보이지만, 그동안은 꽤 높았습니다 GET 요청을 하는 간단한 함수를 작성해 보겠습니다.

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

이제 이를 사용해 보겠습니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

이제 XMLHttpRequest를 수동으로 입력하지 않고도 HTTP 요청을 할 수 있습니다. 이는 XMLHttpRequest의 짜증나는 낙타 대소문자까지 감안하면 할수록 인생이 더 행복해질 것입니다.

체이닝

then()가 스토리의 끝이 아닙니다. then를 연결하여 사용할 수 있습니다. 값을 변환하거나 추가 비동기 작업을 차례로 실행할 수 있습니다.

가치 변환

새 값을 반환하기만 하면 값을 변환할 수 있습니다.

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

실제 예를 들어 다시 돌아가 보겠습니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
})

응답은 JSON이지만 현재 일반 텍스트로 받고 있습니다. get 함수를 변경하여 JSON을 사용할 수도 있습니다. responseType님, 하지만 프라미스로 해결할 수도 있습니다.

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

JSON.parse()는 단일 인수를 사용하고 변환된 값을 반환하므로 단축키를 만들 수 있습니다.

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

실제로 getJSON() 함수를 정말 쉽게 만들 수 있습니다.

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON()는 URL을 가져온 후 파싱하는 프로미스를 계속 반환합니다. JSON 형식으로 반환합니다

비동기 작업을 큐에 추가

then를 연결하여 비동기 작업을 순서대로 실행할 수도 있습니다.

then() 콜백에서 무언가를 반환하는 것은 마술과 약간 비슷합니다. 값을 반환하면 해당 값으로 다음 then()가 호출됩니다. 하지만 프라미스와 유사한 것을 반환하면 다음 then()가 그 것을 기다리고 해당 프라미스가 해결 (성공/실패)될 때만 호출됩니다. 예를 들면 다음과 같습니다.

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

여기서는 story.json에 비동기 요청을 하여 첫 번째 URL을 요청합니다. 이 것은 프라미스가 단순한 콜백 패턴에서 눈에 띄기 시작합니다.

바로가기 메서드를 만들어 챕터를 가져올 수도 있습니다.

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

getChapter가 호출될 때까지 story.json를 다운로드하지 않지만 시간 getChapter는 스토리 프로미스를 재사용하므로 story.json 한 번만 가져올 수 있습니다 약속해!

오류 처리

앞서 살펴본 것처럼 then()는 성공을 위한 인수와 성공을 위한 인수, 이렇게 두 개의 인수를 취합니다. (또는 promise-speak에서 처리 및 거부) 다음과 같이 처리합니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

catch()를 사용할 수도 있습니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch()에 대한 특별한 게 없죠. 그냥 설탕과요 then(undefined, func)이지만 더 읽기 쉽습니다. 참고: 두 개의 코드는 위의 예는 동일하지 않으며 후자는 다음과 같습니다.

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

차이는 미묘하지만 매우 유용합니다. 프로미스 거부 건너뛰기 거부 콜백 (또는 catch().then() 동일합니다). then(func1, func2)을(를) 사용하면 func1 또는 func2이(가) 다음과 같이 됩니다. 둘 다 호출할 수는 없습니다 하지만 then(func1).catch(func2)를 사용하면 둘 다 체인의 별도의 단계이므로 func1가 거부되면 호출됩니다. 탑승 다음과 같습니다.

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

위의 흐름은 일반적인 JavaScript try/catch와 매우 유사하며, '시도' 중에 발생하는 경우 즉시 catch() 블록으로 이동합니다. 사용 가능한 (플로 차트를 좋아하기 때문에)를 플로우 차트로 만듭니다.

프라미스 처리 시에는 파란색 선을, 프라미스인 경우 적색선을 따르면 됩니다. 거부합니다.

JavaScript 예외 및 프로미스

프라미스가 명시적으로 거부되지만 암시적으로 거부될 때 거부가 발생합니다. 생성자 콜백에서 오류가 발생한 경우:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

즉, 프라미스 생성자 콜백을 사용하므로 오류가 자동으로 포착되고 거부될 수 있습니다.

then() 콜백에서 발생한 오류도 마찬가지입니다.

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

실제 오류 처리

스토리와 챕터로 catch를 사용하여 사용자에게 오류를 표시할 수 있습니다.

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

story.chapterUrls[0] 가져오기에 실패하는 경우 (예: http 500 또는 사용자가 오프라인 상태) 다음 성공 콜백을 모두 건너뛰며, 여기에는 getJSON()는 응답을 JSON으로 파싱하려고 시도하고 chapter1.html을 추가하는 콜백입니다. 대신 캐치로 이동합니다. 있습니다. 그 결과 'Failed to show chapter'가 표시됩니다. 가 페이지에 추가됩니다. 이전 작업 중 하나라도 실패했습니다

JavaScript의 try/catch와 마찬가지로 오류가 포착되고 후속 코드가 포착됩니다. 스피너가 항상 숨겨지도록 하고 싶습니다. 이 위 버전은 다음의 비차단 비동기 버전이 됩니다.

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

복구하지 않고 단순히 로깅 목적으로 catch()하는 것이 좋습니다. 삭제합니다. 이렇게 하려면 오류를 다시 발생시키면 됩니다. 이 작업은 getJSON() 메서드:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

한 장을 가져왔지만 모두 가져오려고 합니다. 만들어 보자 발생할 수 있습니다.

동시 로드 및 시퀀싱: 둘 다 최대한 활용하기

비동기를 생각하는 것은 쉽지 않습니다. 시작하는 데 어려움을 겪고 있다면 동기 함수인 것처럼 코드를 작성해 보세요. 이 경우에는 다음과 같습니다.

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

맞습니다! 하지만 동기화가 이루어지고 다운로드되는 동안 브라우저가 잠깁니다. 받는사람 이 작업을 비동기식으로 만들기 위해 then()를 사용하여 순서대로 발생합니다.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch &amp; display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

어떻게 하면 챕터 URL을 반복하고 순서대로 가져올 수 있을까요? 이 작동하지 않는 경우:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach는 비동기를 인식하지 않으므로 챕터가 어떤 순서로든 표시됩니다. 다운로드하며 이는 기본적으로 펄프 픽션이 작성된 방식입니다. 이것은 펄프 픽션, 이제 고쳐 봅시다.

시퀀스 만들기

chapterUrls 배열을 프라미스 시퀀스로 바꾸려고 합니다. then()를 사용하면 됩니다.

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

Promise.resolve()를 처음 보는 것은 처음입니다. 약속할 수 있어야 합니다. 만약 Promise 인스턴스는 그냥 반환합니다. (참고: 이는 일부 구현이 아직 따르지 않는 사양으로 변경될 수 있음). 만약 프라미스와 유사한 것 (then() 메서드 있음)을 전달하면 같은 방식으로 처리/거부하는 진짜 Promise입니다. 만약 다른 값(예: Promise.resolve('Hello')이면 약속을 할 수 있습니다. 값을 값 없이 호출하면 'undefined'를 사용하여 처리합니다.

다음 명령어로 거부되는 프로미스를 만드는 Promise.reject(val)도 있습니다. 지정할 수 있습니다 (또는 정의되지 않음).

위의 코드는 array.reduce:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

이전 예와 동일하게 실행하지만 별도의 'sequence' 변수의 값을 지정합니다. 배열의 각 항목에 대해 감소 콜백이 호출됩니다. 'sequence' 처음에는 Promise.resolve()이지만 'sequence'를 호출합니다. 이전 호출에서 반환된 값입니다. array.reduce 배열을 단일 값으로 압축하는 데 정말 유용합니다. 이 경우에는 약속입니다.

이제 모든 내용을 종합해 보겠습니다.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

그리고 동기화 버전의 완전 비동기 버전이 있습니다. 하지만 개선할 수 있습니다 현재 페이지는 다음과 같이 다운로드되고 있습니다.

브라우저는 한 번에 여러 항목을 다운로드하는 데 매우 능숙하기 때문에 챕터를 차례로 다운로드함으로써 성능을 향상할 수 있습니다. 이제 모두 동시에 다운로드하고, 모두 도착하면 처리합니다. 다행히 이를 위한 API가 있습니다.

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all는 프로미스 배열을 취하고 처리하는 프로미스를 만듭니다. 완료되었을 수 있습니다. 어떤 결과가 될 수 있는지 전달된 프로미스와 동일한 순서로 표시됩니다.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

연결에 따라 하나씩 로드하는 것보다 몇 초 빠를 수 있습니다. 그리고 첫 번째 시도보다 코드가 적습니다. 챕터는 어떤 형식이든 화면에 올바른 순서로 표시됩니다.

하지만 인식되는 성능을 개선할 수는 있습니다. 1장이 도착하면 페이지에 추가해야 합니다. 이렇게 하면 사용자가 챕터가 도착했습니다. 3장이 도착하면 사용자가 2장이 누락되었음을 인식하지 못할 수 있기 때문입니다. 2장이 2장, 3장 등을 추가할 수 있습니다.

이를 위해 모든 챕터의 JSON을 동시에 가져온 다음 시퀀스를 사용하여 문서에 추가합니다.

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

자, 이 두 가지의 가장 큰 장점이 여기 있습니다. 게재 시간이 동일합니다. 사용자는 첫 번째 콘텐츠를 더 빨리 가져옵니다.

이 사소한 예에서 모든 장이 거의 동시에 도착하지만 한 번에 하나씩 표시할 때의 이점은 더 많고 커질수록 과장됩니다. 챕터별로 구성되어 있습니다.

Node.js 스타일 콜백 또는 이벤트가 더 중요한 것은 따라 하기가 쉽지 않다는 것입니다. 그러나 다른 ES6 기능과 결합될 때 프라미스 스토리의 끝이 아닙니다. 더 쉽게 만들 수 있습니다.

보너스: 기능 확대

이 글을 처음 작성한 이후로 프로미스를 사용할 수 있는 기능이 확장되어 크게 향상되었습니다. Chrome 55부터 비동기 함수를 통해 프로미스 기반 코드가 동기식인 것처럼 작성되었지만 기본 스레드를 차단하지 않습니다. 다음과 같은 작업을 할 수 있습니다. 자세한 내용은 내 비동기 함수 도움말을 참고하세요. 이 주요 브라우저에서 프로미스와 비동기 함수를 모두 광범위하게 지원합니다. 자세한 내용은 MDN의 약속async 함수 참조

Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, 아디 오스마니, 아서 에반스, 유타카 히라노는 이 교정을 통해 도움이 됩니다.

또한 도움을 주신 Mathias Bynens업데이트하여 확인할 수 있습니다