JavaScript 异步进化史

同步与异步

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

例 1 java

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

在上述代码中,A、B、C 是三个不一样的函数,每一个函数都是一个不相关的任务。在同步模式下,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分状况,同步模式都没问题。可是若是 B 任务是一个耗时很长网络的请求,而 C 任务刚好是展示新页面,B 与 C 没有依赖关系。这就会致使网页卡顿的现象。有一种解决方案,将 B 放在 C 后面去执行,但惟一有些不足的是,B 的网络请求会迟一些再发送。node

还有另外一种更完美解决方案,将 B 任务分红的两个部分。一部分是,当即执行网络请求的任务;另外一部分是,在请求数据回来后执行的任务。这种一部分在当即执行,另外一部分在将来执行的模式称为 异步(asynchronous) 。伪代码以下:ajax

例 2 promise

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

实际上,JavaScript 引擎先执行了调用了浏览器的网络请求接口的任务(一部分任务),再由浏览器发送网络请求并监听请求返回(这个任务不禁 JavaScript 引擎执行,而是浏览器);等请求放回后,浏览器再通知 JavaScript 引擎,开始执行回调函数中的任务(另外一部分)。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。浏览器

callback

将来执行的函数一般也叫 callback。使用 callback 的异步模式,解决了阻塞的问题,可是也带了一些其余问题。在最开始,咱们的函数是从上往下书写的,也是从上往下执行的,这很是符合咱们的思惟习惯,可是如今却被 callback 打断了!在上面一段代码中,它跳过 B 任务,先执行了 C任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,所以也更容易滋生 BUG。网络

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

例 3 并发

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

// 下面是答案,你猜对了吗?
// A => E => B => D => C

在例 3 中,咱们的阅读代码视线是 A => B => C => D => E ,可是执行顺序倒是 A => E => B => D => C 。从上往下执行的顺序被 Callback 打乱了,这就是非线性代码带来的糟糕之处。异步

上面的例子中,咱们能够经过将 ajax 后面执行的任务 E 和 任务 D 提早,来进行代码优化。这种技巧在写多重嵌套的代码时,是很是有用的。改进后,以下。

例 4

A();
E();
ajax('url1', function(){
    B();
    D();
    ajax('url2', function(){
        C();
    }
});
// 稍做优化,代码更容易看懂
// A => E => B => D => C

在例 4 中,只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。

例 5

A();

ajax('url1', function(){
    B();

    ajax('url2', function(){
        C();
    },function(){
        D();
    });

},function(){
    E();

});

例 5 中,加上异常处理回调后,url1 的成功回调函数 B 和异常回调函数 E,被分开了。这种“非线性”的状况又出现了。

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

例 6

A();

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

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

到此,callback 引发的“非线性”问题基本获得解决。遗憾的是,一旦嵌套层数多起来,阅读起来还不是很方便。此外,callback 一旦出现异常,只能在当前回调内部处理异常,并无一个总体的异常触底方案。

promise

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

Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。签定协议的两方分别是异步接口和 callback。首先 Promise 和异步接口签定一个协议,成功时,调用 resolve 函数通知 Promise,异常时,调用 reject 通知 Promise。另外一方面 Promise 和 callback 也签定一个协议,当异步接口的 resolvereject 被调用时,由 Promise 返回可信任的值给 thencatch 中注册的 callback。

一个最简单的 promise 示例以下:

例 7

// 建立一个 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 必定会被异步调用,且只会调用一次。好比在使用第三方库的时候,因为某些缘由,(假的)“异步”接口不可靠,它执行了同步代码,而没有进入异步逻辑,如例 8。

例 8

var promise1 = new Promise(function (resolve) {
  // 因为某些缘由致使“异步”接口,被同步执行了
  if (true ){
    // 同步代码
    resolve('B');
  } else {
    // 异步代码
    setTimeout(function(){
      resolve('B');
    },0)
  }

});

// promise依旧会异步执行
promise1.then(function(value){
    console.log(value)
});

console.log('A');
// A => B (先 A 后 B)

再好比,因为某些缘由,异步接口不可靠,resolvereject 被执行了两次。但 Promise 只会通知 callback ,第一次异步接口返回的结果。如例 9:

例 9

var promise2 = new Promise(function (resolve) {
  // resolve 被执行了 2 次
  setTimeout(function(){
    resolve("第一次");
  },0)
  setTimeout(function(){
    resolve("第二次");
  },0)
});

// 但 callback 只会被调用一次,
promise2.then(function(msg){
    console.log(msg) // "第一次"
    console.log('A')
});
// A (只有一个)

介绍完 Promise 的特性后,来看看它如何利用链式调用,解决 callback 模式下,异步代码可读性的问题。链式调用指的是:函数 return 一个能够继续执行的对象,该对象能够继续调用,而且 return 另外一个能够继续执行的对象,如此反复达到不断调用的结果。如例 10:

例 10

// return 一个能够继续执行的 Promise 对象
var fetch = function(url){
    return new Promise(function (resolve,reject) {
        ajax(url,resolve,reject);
    });
}

A();
fetch('url1').then(function(){
    B();
    // 返回一个新的 Promise 实例
    return fetch('url2');
}).catch(function(){
    C();
    // 异常的时候也能够返回一个新的 Promise 实例
    return fetch('url2');
    // 使用链式写法调用这个新的 Promise 实例的 then 方法
}).then(function() {
    // 能够继续 return,也能够不继续 return,结束链式调用
    D();
})
// A B C D (顺序执行)

如此反复,不断返回一个 Promise 对象,使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。

另外,Promise 还解决了一个难点,callback 只能捕获当前错误异常。Promise 和 callback 不一样,每一个 callback 只能知道本身的报错状况,但 Promise 代理着全部的 callback,全部 callback 的报错,均可以由 Promise 统一处理。因此,能够经过在最后设置一个 catch 来捕获以前未捕获异常。

Promise 解决 callback 的异步调用问题,但 Promise 并无摆脱 callback,它只是将 callback 放到一个能够信任的中间机构,这个中间机构去连接 callback 和异步接口。此外,链式调用的写法并非很是优雅。接下来介绍的异步(async)函数方案,会给出一个更好的解决方案。

异步(async)函数

异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让咱们摆脱 callback 的束缚,直接用“同步”方式,写异步函数。注意,这里的同步指的是写法同步,但实际依旧是异步执行的。

声明异步函数,只需在普通函数前添加一个关键字 async 便可,如:

async function main(){}

在异步函数中,可使用 await 关键字,表示等待后面表达式的执行结果,再往下继续执行。表达式通常都是 Promise 实例。如,例 11:

例 11

var  timer = function (delay) {
  return new Promise(function create(resolve,reject) {
    if(typeof delay !== 'number'){
      reject(new Error('type error'));
    }
    setTimeout(resolve,delay,'done');
  });
}

async function main{
    var value = await timer(100);
    // 不会马上执行,等待 100ms 后才开始执行
    console.log(value);  // done
}

main();

异步函数和普通函数的调用方式同样,最早执行 main() 函数。以后,会当即执行 timer(100) 函数。等到( await )后面的 promise 函数( timer(100) )返回结果后,程序才会执行下一行代码。

异步函数和普通函数写法基本相似,除了前面提到的声明方式相似和调用方式同样以外,它也可使用 try...catch 来捕捉异常,也能够传入参数。但在异步函数中使用 return 是没有做用的,这和普通的 callback 函数 return 没有做用是同样缘由。callback 或者异步函数是单独放在 JavaScript 栈(stack)中执行的,这时同步代码已经执行完毕。

在异步函数中,使用 try...catch 异常捕获的方案,代替了 Promise catch 的异常捕获的方案。示例以下:

例 12

async function main(delay){
  try{
    // timer 在例 11 中有过声明
    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,否则异步函会被同步执行。例子以下:

例 12

async function doAsync(delay){
    // timer 在例 11 中有过声明
    var value1 = await timer(delay);
    console.log('A')
}

async function main(main){
  doAsync(0);
  console.log('B')
}

main(main);
// B A

这个时候打印出来的值是 B A。说明 doAsync 函数中的 await timer(delay) 并被同步执行了。若是要正确异步地执行 doAsync 函数,须要该函数以前添加 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

因为异步函数采用类同步的书写方法,因此在处理多个并发请求,新手可能会像下面同样书写:

例 13

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();

但这样会致使 url2 的请求必需等到 url1 的请求回来后才会发送。若是 url1url2 没有相互的依赖关系,将这两个请求同时发送实现的效果会更好。

Promise.all 的方法,能够很好的处理并发请求。Promise.all 接受将多个 Promise 实例为参数,并将这些参数包装成一个新的 Promise 实例。这样,Promise.all 中全部的请求会第一时间发送出去;在全部的请求成功回来后才会触发 Promise.allresolve 函数;当有一个请求失败,则当即调用 Promise.allreject 函数。

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();

最后对异步函数的内容作个小结:

  • 声明: async function main(){}

  • 异步函数逻辑:可使用 await

  • 调用: main()

  • 捕获异常: try...catch

  • 传入参数: main('第一个参数')

  • return:不生效

  • 异步函数做为参数传入其余函数:能够

  • 处理并发逻辑:Promise.all

目前使用最新的 Chrome/node 已经支持 ES7 异步函数的写法了,另外也能够经过 Babel 以将异步函数转义为 ES5 的语法执行。你们能够本身动手试试,使用异步函数,用类同步的方式,书写异步代码。

相关文章
相关标签/搜索