前言

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. .net 图片上传

      /// <summary>      /// asp.net上传图片并生成缩略图      /// </summary>      /// <param name=& ...

  2. VS2010保存时控件验证(用onclientclick事件) js脚本

    控件按钮代码: asp:Button ID="btnSave" runat="server" OnClick="btnSave_Click" ...

  3. VisualSVN错误 Cannot query proxy blanket解决办法

    最近重新做了一下系统,在安装和使用svn过程中遇到了一些问题,记下备忘. 第一次安装好系统之后,安装VisualSVN遇到报错: Custom action InstallWMISchemaExcut ...

  4. C# DataTable转实体 通用方法

    public static T GetEntity<T>(DataTable table) where T : new() { T entity = new T(); foreach (D ...

  5. JSP判断闰年

    闰年条件: 能被4整除但不能被100整除 能被400整除 <%@ page language="java" import="java.util.*" co ...

  6. Batch Normalization 学习笔记

    原文:http://blog.csdn.net/happynear/article/details/44238541 今年过年之前,MSRA和Google相继在ImagenNet图像识别数据集上报告他 ...

  7. 时间戳Id

    ID:格式据1970.1.1毫秒数 1535091029740  13位 问题:高并发,分布式明显会有问题,网上有雪花算法,但是位数跟我需要的不一样,暂时不考虑,以后研究再用. 参考: https:/ ...

  8. WinForm中实现HotKey

    最近在写一个游戏辅助工具,来点Win变成的总结 主要用了RegisterHotKey:UnregisterHotKey:两个winAPI 以下代码来自stackoverflow新增了一个HotKeyM ...

  9. flexbox父盒子align-content属性

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. vuejs中使用echart图表

    首先安装echart npm i echarts -S 加下来以使用这个图表为例 在vue组件中像这样使用: <template> <div :class="classNa ...