비동기식?
비동기(Asynchronous)라는 말을 여러 곳에서 본 적이 있지만, 이 개념을 완전히 이해하지 못할 때에는 "이미 명확하다"고 생각하는 경우가 많습니다(*  ̄? ̄ ).
비슷한 상황이시라면 이 단어로 검색하시면 대략적인 설명을 얻으실 수 있습니다. 여기서는 JavaScript의 비동기성에 대해 조금 더 설명하겠습니다.
이 코드를 보세요:
var start = new Date(); setTimeout(function(){ var end = new Date(); console.log("Time elapsed: ", end - start, "ms"); }, 500); while (new Date - start < 1000) {};
이 코드를 실행하면 Time elapsed: 1013ms와 같은 결과를 얻게 됩니다. setTimeout()에 의해 앞으로 500ms 동안 실행되도록 설정된 함수는 실제로 실행되기까지 1000ms 이상을 기다린다.
어떻게 설명해야 할까요? setTimeout()이 호출되면 지연된 이벤트가 대기열에 추가됩니다. 그런 다음 더 이상 코드가 없을 때까지 이 이후의 코드를 계속 실행하고, 그 이후의 코드를 계속 실행합니다. 코드가 없으면 JavaScript 스레드는 유휴 상태가 됩니다. 이때 JavaScript 실행 엔진은 대기열을 살펴보고 대기열에서 "트리거되어야 하는" 이벤트를 찾은 다음 이 이벤트의 핸들러(함수)를 호출합니다. 프로세서는 실행을 완료한 후 큐로 돌아가서 다음 이벤트를 살펴봅니다.
단일 스레드 JavaScript는 대기열을 통해 이벤트 루프 형태로 작동합니다. 따라서 이전 코드에서는 코드가 실행되는 동안 최대 1000ms 동안 실행 엔진을 드래그하는 데 while을 사용했으며 모든 코드가 완료되어 큐에 반환될 때까지 이벤트가 트리거되지 않습니다. 이것이 JavaScript의 비동기 메커니즘입니다.
JavaScript의 비동기 문제
JavaScript의 비동기 작업이 항상 간단하지는 않을 수 있습니다.
Ajax는 아마도 우리가 가장 많이 사용하는 비동기 작업일 것입니다. jQuery를 예로 들면, Ajax 요청을 시작하는 코드는 일반적으로 다음과 같습니다.
// Ajax请求示意代码 $.ajax({ url: url, data: dataObject, success: function(){}, error: function(){} });
이런 방식으로 작성하는 데 문제가 있나요? 간단히 말해서, 휴대성이 충분하지 않습니다. 요청이 시작될 때 성공 및 오류 콜백을 작성해야 하는 이유는 무엇입니까? 내 콜백이 매우 많은 일을 수행해야 한다면 여기로 돌아가서 한 가지 생각이 날 때 코드를 추가해야 합니까?
또 다른 예로 Ajax 액세스를 위한 URL 주소가 4개 있습니다. 먼저 Ajax를 통해 첫 번째 액세스가 완료된 후 반환된 주소를 사용합니다. 두 번째 매개변수로 얻은 데이터를 다시 액세스하고, 두 번째 액세스가 완료된 후 세 번째 매개변수에 액세스합니다... 이제부터 4번의 액세스가 모두 완료됩니다. 이렇게 작성하면 이렇게 되는 것 같습니다.
$.ajax({ url: url1, success: function(data){ $.ajax({ url: url2, data: data, success: function(data){ $.ajax({ //... }); } }); } })
Pyramid of Doom이라는 코드가 이렇게 생겼다고 생각하실 겁니다. 끔찍한. 직접 연결된 콜백을 작성하는 데 익숙하다면 한 이벤트에서 다음 이벤트로 전달되는 비동기 이벤트에 대해 혼란스러울 수 있습니다. 이러한 콜백 함수에 별도의 이름을 지정하고 별도로 저장하면 형식의 중첩이 줄어들고 코드가 더 명확해질 수 있지만 여전히 문제가 해결되지는 않습니다.
또 다른 일반적인 어려움은 두 개의 Ajax 요청을 동시에 보낸 후 두 요청이 모두 성공적으로 반환된 후 다음 작업을 수행하기 위해 이전 방법을 따른다면 생각해 보세요. 호출 위치에 콜백을 첨부하는 것이 조금 어려운 것 같나요?
Promise는 이러한 비동기 작업을 처리하는 데 적합하며 더 우아한 코드를 작성할 수 있게 해줍니다.
무대에 오르는 약속
약속이란? 계속해서 이전 jQuery Ajax 요청 신호 코드를 예로 들어 보겠습니다. 해당 코드는 실제로 다음과 같이 작성할 수 있습니다.
var promise = $.ajax({ url: url, data: dataObject }); promise.done(function(){}); promise.fail(function(){});
이것은 이전 코드와 유사합니다. Ajax 요청 신호는 동일합니다. 보시다시피 Promise를 추가하면 코드 형식이 변경됩니다. Ajax 요청은 변수 할당과 마찬가지로 "저장"됩니다. 이것이 캡슐화이며, 캡슐화는 실제로 비동기 이벤트를 더 쉽게 만듭니다.
캡슐화가 유용합니다
Promise 객체는 비동기 이벤트에 대한 캡슐화된 참조와 같습니다. 이 비동기 이벤트가 완료된 후 뭔가를 하고 싶으십니까? 콜백만 첨부하면 됩니다. 아무리 많이 첨부해도 상관없습니다!
jQuery의 Ajax 메소드는 Promise 객체를 반환합니다(이는 jQuery 1.5에 추가된 핵심 기능입니다). 비동기 이벤트가 성공적으로 완료된 후 실행하고 싶은 두 개의 함수 do1() 및 do2()가 있는 경우 다음만 수행하면 됩니다.
promise.done(do1); // Other code here. promise.done(do2);
이 방법은 훨씬 더 무료입니다. 비동기 이벤트가 시작되는 위치에 관계없이 코드를 작성하는 동안 언제든지 이 Promise 객체를 저장하고 콜백을 원하는 만큼 연결하면 됩니다. 이것이 Promise의 장점입니다.
공식 소개
Promise는 비동기 작업에 매우 유용하여 Promises/A라는 CommonJS 사양으로 개발되었습니다. Promise는 작업이 완료된 후 반환 값을 나타냅니다.
Promise 작업이 성공했음을 나타내는 양수(이행 또는 해결)의 세 가지 상태가 있습니다.
부정(거부 또는 실패)은 Promise 작업이 실패했음을 나타냅니다.
대기 중(보류 중), 아직 긍정적이거나 부정적인 결과가 나오지 않고 진행 중입니다.
또한 Promise 작업의 성공 또는 실패를 나타내는 데 사용되는 명목 상태가 있는데, 이는 확정이라고 하는 긍정적인 상태와 부정적인 상태의 모음입니다. Promise에는 다음과 같은 중요한 기능도 있습니다:
一个Promise只能从等待状态转变为肯定或否定状态一次,一旦转变为肯定或否定状态,就再也不会改变状态。
如果在一个Promise结束(成功或失败,同前面的说明)后,添加针对成功或失败的回调,则回调函数会立即执行。
想想Ajax操作,发起一个请求后,等待着,然后成功收到返回或出现错误(失败)。这是否和Promise相当一致?
进一步解释Promise的特性还有一个很好的例子:jQuery的$(document).ready(onReady)。其中onReady回调函数会在DOM就绪后执行,但有趣的是,如果在执行到这句代码之前,DOM就已经就绪了,那么onReady会立即执行,没有任何延迟(也就是说,是同步的)。
Promise示例
生成Promise
Promises/A里列出了一系列实现了Promise的JavaScript库,jQuery也在其中。下面是用jQuery生成Promise的代码:
var deferred = $.Deferred(); deferred.done(function(message){console.log("Done: " + message)}); deferred.resolve("morin"); // Done: morin
jQuery自己特意定义了名为Deferred的类,它实际上就是Promise。$.Deferred()方法会返回一个新生成的Promise实例。一方面,使用deferred.done()、deferred.fail()等为它附加回调,另一方面,调用deferred.resolve()或deferred.reject()来肯定或否定这个Promise,且可以向回调传递任意数据。
合并Promise
还记得我前文说的同时发送2个Ajax请求的难题吗?继续以jQuery为例,Promise将可以这样解决它:
var promise1 = $.ajax(url1), promise2 = $.ajax(url2), promiseCombined = $.when(promise1, promise2); promiseCombined.done(onDone);
$.when()方法可以合并多个Promise得到一个新的Promise,相当于在原多个Promise之间建立了AND(逻辑与)的关系,如果所有组成Promise都已成功,则令合并后的Promise也成功,如果有任意一个组成Promise失败,则立即令合并后的Promise失败。
级联Promise
再继续我前文的依次执行一系列异步任务的问题。它将用到Promise最为重要的.then()方法(在Promises/A规范中,也是用“有then()方法的对象”来定义Promise的)。代码如下:
var promise = $.ajax(url1); promise = promise.then(function(data){ return $.ajax(url2, data); }); promise = promise.then(function(data){ return $.ajax(url3, data); }); // ...
Promise的.then()方法的完整形式是.then(onDone, onFail, onProgress),这样看上去,它像是一个一次性就可以把各种回调都附加上去的简便方法(.done()、.fail()可以不用了)。没错,你的确可以这样使用,这是等效的。
但.then()方法还有它更为有用的功能。如同then这个单词本身的意义那样,它用来清晰地指明异步事件的前后关系:“先这个,然后(then)再那个”。这称为Promise的级联。
要级联Promise,需要注意的是,在传递给then()的回调函数中,一定要返回你想要的代表下一步任务的Promise(如上面代码的$.ajax(url2, data))。这样,前面被赋值的那个变量才会变成新的Promise。而如果then()的回调函数返回的不是Promise,则then()方法会返回最初的那个Promise。
应该会觉得有些难理解?从代码执行的角度上说,上面这段带有多个then()的代码其实还是被JavaScript引擎运行一遍就结束。但它就像是写好的舞台剧的剧本一样,读过一遍后,JavaScript引擎就会在未来的时刻,依次安排演员按照剧本来演出,而演出都是异步的。then()方法就是让你能写出异步剧本的笔。
将Promise用在基于回调函数的API
前文反复用到的$.ajax()方法会返回一个Promise对象,这其实只是jQuery特意提供的福利。实际情况是,大多数JavaScript API,包括Node.js中的原生函数,都基于回调函数,而不是基于Promise。这种情况下使用Promise会需要自行做一些加工。
这个加工其实比较简单和直接,下面是例子:
var deferred = $.Deferred(); setTimeout(deferred.resolve, 1000); deferred.done(onDone);
这样,将Promise的肯定或否定的触发器,作为API的回调传入,就变成了Promise的处理模式了。
Promise是怎么实现出来的?
本文写Promise写到这里,你发现了全都是基于已有的实现了Promise的库。那么,如果要自行构筑一个Promise的话呢?
位列于Promises/A的库列表第一位的Q可以算是最符合Promises/A规范且相当直观的实现。如果你想了解如何做出一个Promise,可以参考Q提供的设计模式解析。
限于篇幅,本文只介绍Promise的应用。我会在以后单独开一篇文章来详述Promise的实现细节。
作为JavaScript后续版本的ECMAScript 6将原生提供Promise,如果你想知道它的用法,推荐阅读JavaScript Promises: There and back again。
结语
약속이라는 단어는 너무 완고해서 번역에 적합하지 않고, 얼핏 보면 의미가 불분명해질 것입니다. 그러나 JavaScript에서 더 복잡한 비동기 작업을 수행할 때 실제로 상당한 도움을 줄 수 있습니다.
위 내용은 비동기 JavaScript 프로그래밍_node.js?1.1.5에서의 Promise 사용 내용입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요!