前言

JS 中最基础的异步调用方式是 callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback。对于简单的异步操作,用 callback 实现,是够用的。但随着负责交互页面和 Node 出现,callback 方案的弊端开始浮现出来。 Promise 规范孕育而生,并被纳入 ES6 的规范中。后来 ES7 又在 Promise 的基础上将 async 函数纳入标准。此为 JavaScript 异步进化史。

同步与异步

通常,代码是由上往下依次执行的。如果有多个任务,就必需排队,前一个任务完成,后一个任务才会执行。这种执行模式称之为:同步(synchronous)。新手容易把计算机用语中的同步,和日常用语中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在计算机中,同步指的是任务从上往下依次执行的模式。比如:

A();
B();
C();

在这段代码中,A、B、C是三个不同的函数,每个函数都是一个不相关的任务。在同步模式,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长的网络请求,而 C 任务恰好是展现新页面,就会导致网页卡顿。

更好解决方案是,将 B 任务分成两个部分。一部分立即执行网络请求的任务,另一部分在请求回来后的执行任务。这种一部分立即执行,另一部分在未来执行的模式称为异步。

A();
// 在现在发送请求
ajax('url1',function B() {
 // 在未来某个时刻执行
})
C();
// 执行顺序 A => C => B

实际上,JS 引擎并没有直接处理网络请求的任务,它只是调用了浏览器的网络请求接口,由浏览器发送网络请求并监听返回的数据。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。

callback 

未来执行的函数通常也叫 callback。使用 callback 的异步模式,解决了阻塞的问题,但是也带来了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这种“线性”模式,非常符合我们的思维习惯,但是现在却被 callback 打断了!在上面一段代码中,现在它跳过 B 任务先执行了 C任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,因此也更容易滋生 BUG。

试着判断下面这段代码的执行顺序,你会对“非线性”代码比“线性”代码更难以阅读,体会更深。

A();

ajax('url1', function(){
   B();    ajax('url2', function(){
       C();
   }
   D();
   
});
E();
// A => E => B => D => C

这段代码中,从上往下执行的顺序被 Callback 打乱了。我们的阅读代码视线是A => B => C => D => E,但是执行顺序却是A => E => B => D => C,这就是非线性代码带来的糟糕之处。

通过将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,被分开了。这种“非线性”的情况又出现了。

在 node 中,为了解决的异常回调导致的“非线性”的问题,制定了错误优先的策略。node 中 callback 的第一个参数,专门用于判断是否发生异常

A();

get('url1', function(error){
   if(error){
       E();
   }else {
       B();        get('url2', function(error){
           if(error){
               D();
           }else{
               C();
           }
       });
   }
});

到此,callback 引起的“非线性”问题基本得到解决。遗憾的是,使用 callback 嵌套,一层层if else和回调函数,一旦嵌套层数多起来,阅读起来不是很方便。此外,callback 一旦出现异常,只能在当前回调函数内部处理异常。

promise

在 JavaScript 的异步进化史中,涌现出一系列解决 callback 弊端的库,而 Promise 成为了最终的胜者,并成功地被引入了 ES6 中。它将提供了一个更好的“线性”书写方式,并解决了异步异常只能在当前回调中被捕获的问题。

Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。首先 Promise 和异步接口签订一个协议,成功时,调用resolve函数通知 Promise,异常时,调用reject通知 Promise。另一方面 Promise 和 callback 也签订一个协议,由 Promise 在将来返回可信任的值给then和catch中注册的 callback。

// 创建一个 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)函数

异步(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 异步进化史的更多相关文章

  1. 【转】JavaScript 异步进化史

    前言 JS 中最基础的异步调用方式是 callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback.对于简单的异步操作 ...

  2. JavaScript异步编程的主要解决方案—对不起,我和你不在同一个频率上

    众所周知(这也忒夸张了吧?),Javascript通过事件驱动机制,在单线程模型下,以异步的形式来实现非阻塞的IO操作.这种模式使得JavaScript在处理事务时非常高效,但这带来了很多问题,比如异 ...

  3. JavaScript异步编程原理

    众所周知,JavaScript 的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着. ...

  4. javascript异步编程的前世今生,从onclick到await/async

    javascript与异步编程 为了避免资源管理等复杂性的问题, javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为 ...

  5. JavaScript异步编程(2)- 先驱者:jsDeferred

    JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascri ...

  6. 对Javascript异步执行的理解

    简单的查看了下Javascript异步编程的代码.按照网上的说法,Javascript异步编程的核心就在于setTimeout.这个系统函数让我们将函数的执行放在了一个指定的新“线程”中.于是本来的顺 ...

  7. Promises与Javascript异步编程

    Promises与Javascript异步编程 转载:http://www.zawaliang.com/2013/08/399.html 在如今都追求用户体验的时代,Ajax应用真的是无所不在.加上这 ...

  8. Javascript 异步加载详解

    Javascript 异步加载详解 本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy ...

  9. javascript异步延时载入及推断是否已载入js/css文件

    <html> <head> <script type="text/javascript"> /**======================= ...

随机推荐

  1. linux中安装typecho的pathinfo配置

    最近,我安装typecho,安装完之后发现,只有首页能够访问,其他的页面报404错误 后来发现时nginx默认情况下不支持pathinfo模式,于是我查找一下资料.终于得到解决. 我的nginx.co ...

  2. json过滤某些属性 之@jsonignore

    Jackson相关: 使用Jackson相关的注解时一定要注意自己定义的属性命名是否规范. 命名不规范时会失去效果.(例如Ename ,Eage 为不规范命名.“nameE”,“ageE”为规范命名) ...

  3. 消息中间件activemq-5.13.0整合spring

    首先说明这里是在qctivemq配置好并启动服务的情况下进行,请先自行配置好.也可关注我的博文(消息中间件qctivemq安全验证配置)进行配置. 1.首先看一下项目结构 2.所需jar包,这里只列出 ...

  4. Bulestacks模拟器Bulestacks.prop文件里中英文对照表

    打开“Bulestacks.prop”文件后可以看到以下内容,根据中英文对照表来修改即可. # begin build properties (开始设置系统性能)# autogenerated by ...

  5. asp.net导出EXCEL的好方法!(好用,导出全部数据)

    1.调用方法: ExportExcel("application/ms-excel", "EXCEL名称.xls", GridView1, this.Page) ...

  6. 关于SpringMVC的文件上传

    关于文件的上传,之前写过2篇文章,基于Struts2框架,下面给出文章链接: <关于Struts2的文件上传>:http://www.cnblogs.com/lichenwei/p/392 ...

  7. Java进阶面试题列表

    面向对象编程的基本理念与核心设计思想 解释下多态性(polymorphism),封装性(encapsulation),内聚(cohesion)以及耦合(coupling). 继承(Inheritanc ...

  8. level 6 - unit4 - 强调句

    强调句 强调实义动词 范围: 一般现在时/一般过去式:肯定句 方法:v.前面加do/does/did 例子: i love you --> i do love you i loved you - ...

  9. tornado入门1

    Tornado web server 是使用Python编写出來的一个极轻量级.高可伸缩性和非阻塞IO的Web服务器软件,著名的 Friendfeed 网站就是使用它搭建的. Tornado 跟其他主 ...

  10. 为什么React事件处理函数必须使用Function.bind()绑定this?

    最近在React官网学习Handling Events这一章时,有一处不是很明白.代码如下: class Toggle extends React.Component { constructor(pr ...