JavaScript异步进化史
<h2>前言</h2> <p>JS 中最基础的异步调用方式是 callback ,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback 。对于简单的异步操作,用 callback 实现,是够用的。但随着负责交互页面和 Node 出现, callback 方案的弊端开始浮现出来。 Promise 规范孕育而生,并被纳入 ES6 的规范中。后来 ES7 又在 Promise 的基础上将 async 函数纳入标准。此为 JavaScript 异步进化史。</p> <h2>同步与异步</h2> <p>通常,代码是由上往下依次执行的。如果有多个任务,就必需排队,前一个任务完成,后一个任务才会执行。这种执行模式称之为:同步( synchronous )。新手容易把计算机用语中的同步,和日常用语中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在计算机中,同步指的是任务从上往下依次执行的模式。比如:</p> <pre> <code class="language-javascript">A(); B(); C();</code></pre> <p>在这段代码中, A 、 B 、 C 是三个不同的函数,每个函数都是一个不相关的任务。在同步模式,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长的网络请求,而 C 任务恰好是展现新页面,就会导致网页卡顿。</p> <p>更好解决方案是,将 B 任务分成两个部分。一部分立即执行网络请求的任务,另一部分在请求回来后的执行任务。这种一部分立即执行,另一部分在未来执行的模式称为异步。</p> <pre> <code class="language-javascript">A(); // 在现在发送请求 ajax('url1',function B() { // 在未来某个时刻执行 }) C(); // 执行顺序 A => C => B</code></pre> <p>实际上,JS 引擎并没有直接处理网络请求的任务,它只是调用了浏览器的网络请求接口,由浏览器发送网络请求并监听返回的数据。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。</p> <h2>callback</h2> <p>未来执行的函数通常也叫 callback 。使用 callback 的异步模式,解决了阻塞的问题,但是也带来了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这种“线性”模式,非常符合我们的思维习惯,但是现在却被 callback 打断了!在上面一段代码中,现在它跳过 B 任务先执行了 C 任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,因此也更容易滋生 BUG。</p> <p>试着判断下面这段代码的执行顺序,你会对“非线性”代码比“线性”代码更难以阅读,体会更深。</p> <pre> <code class="language-javascript">A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E(); // A => E => B => D => C</code></pre> <p>这段代码中,从上往下执行的顺序被 Callback 打乱了。我们的阅读代码视线是 A => B => C => D => E ,但是执行顺序却是 A => E => B => D => C ,这就是非线性代码带来的糟糕之处。</p> <p>通过将 ajax 后面执行的任务提前,可以更容易看懂代码的执行顺序。虽然代码因为嵌套看起来不美观,但现在的执行顺序却是从上到下的“线性”方式。这种技巧在写多重嵌套的代码时,是非常有用的。</p> <pre> <code class="language-javascript">A(); E(); ajax('url1', function(){ B(); D(); ajax('url2', function(){ C(); } }); // A => E => B => D => C</code></pre> <p>上一段代码只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。</p> <pre> <code class="language-javascript">A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); },function(){ D(); }); },function(){ E(); });</code></pre> <p>加上异常处理回调后, url1 的成功回调函数 B 和异常回调函数 E ,被分开了。这种“非线性”的情况又出现了。</p> <p>在 Node 中,为了解决的异常回调导致的“非线性”的问题,制定了错误优先的策略。Node 中 callback 的第一个参数,专门用于判断是否发生异常。</p> <pre> <code class="language-javascript">A(); get('url1', function(error){ if(error){ E(); }else { B(); get('url2', function(error){ if(error){ D(); }else{ C(); } }); } });</code></pre> <p>到此, callback 引起的“非线性”问题基本得到解决。遗憾的是,使用 callback 嵌套,一层层 if else 和回调函数,一旦嵌套层数多起来,阅读起来不是很方便。此外, callback 一旦出现异常,只能在当前回调函数内部处理异常。</p> <h2>Promise</h2> <p>在 JavaScript 的异步进化史中,涌现出一系列解决 callback 弊端的库,而 Promise 成为了最终的胜者,并成功地被引入了 ES6 中。它将提供了一个更好的“线性”书写方式,并解决了异步异常只能在当前回调中被捕获的问题。</p> <p>Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。首先 Promise 和异步接口签订一个协议,成功时,调用 resolve 函数通知 Promise,异常时,调用 reject 通知 Promise。另一方面 Promise 和 callback 也签订一个协议,由 Promise 在将来返回可信任的值给 then 和 catch 中注册的 callback 。</p> <pre> <code class="language-javascript">// 创建一个 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 })</code></pre> <p>Promise 是个非常不错的中介,它只返回可信的信息给 callback 。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。</p> <pre> <code class="language-javascript">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)</code></pre> <p>介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。</p> <pre> <code class="language-javascript">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 ...</code></pre> <p>如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。</p> <p>Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback ,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过 catch 来捕获之前未捕获的异常。</p> <p>Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback ,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。</p> <h2>异步(async)函数</h2> <p>异步( async )函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。</p> <p>声明异步函数,只需在普通函数前添加一个关键字 async 即可,如 async function main(){} 。在异步函数中,可以使用 await 关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。</p> <pre> <code class="language-javascript">async function main{ // timer 是在上一个例子中定义的 var value = await timer(100); console.log(value); // done (100ms 后返回 done) } main();</code></pre> <p>异步函数和普通函数一样调用 main() 。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100) 。等到异步执行完成后,才会执行下一行代码。</p> <p>除此之外,异步函数和其他函数基本类似,它使用 try...catch 来捕捉异常。也可以传入参数。但不要在异步函数中使用 return 来返回值。</p> <pre> <code class="language-javascript">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);</code></pre> <p>异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用 await ,异步函数会被同步执行。</p> <pre> <code class="language-javascript">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</code></pre> <p>这个时候打印出来的值是 B A 。说明 doAsync 函数并没有等待 main 的异步执行完毕就执行了 console 。如果要让 console 在 main 的异步执行完毕后才执行,我们需要在 main 前添加关键字 await 。</p> <pre> <code class="language-javascript">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</code></pre> <p>由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致 url2 的请求必需等到 url1 的请求回来后才会发送。</p> <pre> <code class="language-javascript">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();</code></pre> <p>使用 Promise.all 的方法来解决这个问题。 Promise.all 用于将多个Promise实例,包装成一个新的 Promis e 实例,当所有的 Promise 成功后才会触发 Promise.all 的 resolve 函数,当有一个失败,则立即调用 Promise.all 的 reject 函数。</p> <pre> <code class="language-javascript">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();</code></pre> <p>目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。</p> <p> </p> <p>来自:div.io/topic/1802</p> <p> </p>
本文由用户 LamBerry 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!