JavaScript异步进化史

LamBerry 4年前
   <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>