JS에서 가장 기본적인 비동기 호출 방식은 콜백으로, 비동기 완료 후 브라우저나 노드에서 콜백 함수 콜백을 전달합니다. 콜백을 호출하도록 JS 엔진에 알립니다. 간단한 비동기 작업의 경우 콜백을 사용하면 충분합니다. 하지만 인터랙티브 페이지와 Node의 등장으로 콜백 솔루션의 단점이 드러나기 시작했습니다. Promise 사양이 탄생하여 ES6 사양에 통합되었습니다. 나중에 ES7에서는 Promise를 기반으로 비동기 기능을 표준에 통합했습니다. 이것이 JavaScript 비동기 진화의 역사입니다.
일반적으로 코드는 위에서 아래로 실행됩니다. 작업이 여러 개인 경우 대기열에 추가되어야 다음 작업이 실행되기 전에 이전 작업이 완료됩니다. 이 실행 모드를 동기식이라고 합니다. 초보자는 컴퓨터 언어의 동기화와 일상 언어의 동기화를 쉽게 혼동할 수 있습니다. 예를 들어, "파일을 클라우드에 동기화"에서 동기화는 "...일관성을 유지"하는 것을 의미합니다. 컴퓨터에서 동기화는 작업이 위에서 아래로 순차적으로 실행되는 모드를 의미합니다. 예:
A(); B(); C();
이 코드에서 A, B, C는 서로 다른 세 가지 함수이며 각 함수는 서로 관련이 없는 작업입니다. 동기 모드에서 컴퓨터는 작업 A, 작업 B, 마지막으로 작업 C를 수행합니다. 대부분의 경우 동기화 모드는 괜찮습니다. 그러나 작업 B가 장기 실행 네트워크 요청이고 작업 C가 우연히 새 페이지를 표시하는 경우 웹 페이지가 정지됩니다.
더 나은 해결책은 작업 B를 두 부분으로 나누는 것입니다. 한 부분은 네트워크에서 요청한 작업을 즉시 수행하고, 다른 부분은 요청이 돌아온 후에 작업을 수행합니다. 한 부분은 즉시 실행되고 다른 부분은 나중에 실행되는 패턴을 비동기식이라고 합니다.
A(); // 在现在发送请求 ajax('url1',function B() { // 在未来某个时刻执行 }) C(); // 执行顺序 A => C => B
실제로 JS 엔진은 네트워크 요청을 직접 처리하지 않고 브라우저의 네트워크 요청 인터페이스를 호출할 뿐이며, 브라우저는 네트워크 요청을 보내고 반환된 데이터를 모니터링합니다. JavaScript 비동기 기능의 핵심은 브라우저나 Node.js의 멀티스레딩 기능입니다.
향후 실행되는 함수를 보통 콜백이라고 합니다. 콜백의 비동기 모드를 사용하면 차단 문제가 해결되지만 다른 문제도 발생합니다. 처음에는 함수가 위에서 아래로 작성되고 위에서 아래로 실행되었습니다. 이 "선형" 모드는 우리의 사고 습관과 매우 일치하지만 이제는 콜백에 의해 중단됩니다! 위의 코드에서는 이제 작업 B를 건너뛰고 작업 C를 먼저 실행합니다! 이러한 종류의 비동기식 "비선형" 코드는 동기식 "선형" 코드보다 읽기가 더 어렵기 때문에 버그가 발생할 가능성이 더 높습니다.
다음 코드의 실행 순서를 판단해 보면 "비선형" 코드가 "선형" 코드보다 읽기 어렵다는 것을 더 깊이 이해하게 될 것입니다.
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E(); // A => E => B => D => C
이 코드에서는 콜백으로 인해 위에서 아래로의 실행 순서가 중단됩니다. 코드를 읽을 때의 시선은 A => C => E => 이것은 선형 코드의 나쁜 점이 아닙니다.
Ajax 이후에 실행되는 작업을 발전시키면 코드의 실행 순서를 더 쉽게 이해할 수 있습니다. 중첩으로 인해 코드가 보기 흉해 보이지만 이제 실행 순서는 위에서 아래로 "선형"입니다. 이 기술은 여러 개의 중첩 코드를 작성할 때 매우 유용합니다.
A(); E(); ajax('url1', function(){ B(); D(); ajax('url2', function(){ C(); } }); // A => E => B => D => C
이전 코드는 성공 콜백만 처리하고 예외 콜백은 처리하지 않습니다. 다음으로, 예외 처리 콜백을 추가하고 코드의 "선형" 실행 문제를 논의합니다.
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); },function(){ D(); }); },function(){ E(); });
예외 처리 콜백을 추가한 후 url1의 성공 콜백 함수 B와 예외 콜백 함수 E가 분리됩니다. 이러한 "비선형" 상황이 다시 나타납니다.
노드에서는 비정상적인 콜백으로 인해 발생하는 '비선형' 문제를 해결하기 위해 오류 우선 전략을 수립했습니다. 노드 내 콜백의 첫 번째 매개변수는 예외 발생 여부를 판별하는 데 특별히 사용됩니다.
A(); get('url1', function(error){ if(error){ E(); }else { B(); get('url2', function(error){ if(error){ D(); }else{ C(); } }); } });
이 시점에서 콜백으로 인해 발생하는 "비선형" 문제는 기본적으로 해결되었습니다. 안타깝게도 콜백 중첩, if else 레이어 및 콜백 함수를 사용하면 중첩된 레이어 수가 늘어나면 읽기가 그다지 편리하지 않습니다. 또한 콜백에서 예외가 발생하면 현재 콜백 함수 내에서만 예외를 처리할 수 있습니다.
JavaScript의 비동기 진화의 역사에서 콜백의 단점을 해결하기 위해 일련의 라이브러리가 등장했고, Promise가 최종 승자가 되어 ES6에 성공적으로 도입되었습니다. 이는 더 나은 "선형" 작성 방법을 제공하고 비동기 예외가 현재 콜백에서만 포착될 수 있는 문제를 해결할 것입니다.
Promise는 신뢰할 수 있는 비동기 결과를 반환하겠다고 약속하는 중개자와 같습니다. 먼저 Promise는 비동기 인터페이스와 계약을 체결합니다. 성공하면 Promise에 알리기 위해 Resolve 함수가 호출됩니다. 반면 Promise와 callback도 계약을 체결하고 Promise는 그때 등록한 콜백에 신뢰할 수 있는 값을 반환하고 앞으로 catch할 것입니다.
// 创建一个 Promise 实例(异步接口和 Promise 签订协议) var promise = new Promise(function (resolve,reject) { ajax('url',resolve,reject); }); // 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议) promise.then(function(value) { // success }).catch(function (error) { // error })
Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。
var promise1 = new Promise(function (resolve) { // 可能由于某些原因导致同步调用 resolve('B'); }); // promise依旧会异步执行 promise1.then(function(value){ console.log(value) }); console.log('A'); // A B (先 A 后 B) var promise2 = new Promise(function (resolve) { // 成功回调被通知了2次 setTimeout(function(){ resolve(); },0) }); // promise只会调用一次 promise2.then(function(){ console.log('A') }); // A (只有一个) var promise3 = new Promise(function (resolve,reject) { // 成功回调先被通知,又通知了失败回调 setTimeout(function(){ resolve(); reject(); },0) }); // promise只会调用成功回调 promise3.then(function(){ console.log('A') }).catch(function(){ console.log('B') }); // A(只有A)
介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。
var fetch = function(url){ // 返回一个新的 Promise 实例 return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } A(); fetch('url1').then(function(){ B(); // 返回一个新的 Promise 实例 return fetch('url2'); }).catch(function(){ // 异常的时候也可以返回一个新的 Promise 实例 return fetch('url2'); // 使用链式写法调用这个新的 Promise 实例的 then 方法 }).then(function() { C(); // 继续返回一个新的 Promise 实例... }) // A B C ...
如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。
Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。
Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。
异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。
声明异步函数,只需在普通函数前添加一个关键字 async 即可,如async function main(){} 。在异步函数中,可以使用await关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。
async function main{ // timer 是在上一个例子中定义的 var value = await timer(100); console.log(value); // done (100ms 后返回 done) } main();
异步函数和普通函数一样调用 main() 。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100) 。等到异步执行完成后,才会执行下一行代码。
除此之外,异步函数和其他函数基本类似,它使用try...catch来捕捉异常。也可以传入参数。但不要在异步函数中使用return来返回值。
var timer = new Promise(function create(resolve,reject) { if(typeof delay !== 'number'){ reject(new Error('type error')); } setTimeout(resolve,delay,'done'); }); async function main(delay){ try{ var value1 = await timer(delay); var value2 = await timer(''); var value3 = await timer(delay); }catch(err){ console.error(err); // Error: type error // at create (<anonymous>:5:14) // at timer (<anonymous>:3:10) // at A (<anonymous>:12:10) } } main(0);
异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用await,异步函数会被同步执行。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ main(0); console.log('B') } doAsync(main); // B A
这个时候打印出来的值是 B A。说明 doAsync 函数并没有等待 main 的异步执行完毕就执行了 console。如果要让 console 在 main 的异步执行完毕后才执行,我们需要在main前添加关键字await。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ await main(0); console.log('B') } doAsync(main); // A B
由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致url2的请求必需等到url1的请求回来后才会发送。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var value1 = await fetch('url1'); var value2 = await fetch('url2'); conosle.log(value1,value2); }catch(err){ console.error(err) } } main();
使用Promise.all的方法来解决这个问题。Promise.all用于将多个Promise实例,包装成一个新的 Promis e实例,当所有的 Promise 成功后才会触发Promise.all的resolve函数,当有一个失败,则立即调用Promise.all的reject函数。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var arrValue = await Promise.all[fetch('url1'),fetch('url2')]; conosle.log(arrValue[0],arrValue[1]); }catch(err){ console.error(err) } } main();
目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。
以上就是JavaScript 异步进化史的内容,更多相关内容请关注PHP中文网(www.php.cn)!