JavaScript异步编程的6种方法

前言

你应该知道,Javascript语言的执行环境是"单线程"(single thread)。
shell

所谓"单线程",就是指一次只能完成一件任务。若是有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
编程


这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),每每就是由于某一段Javascript代码长时间运行(好比死循环),致使整个页面卡在这个地方,其余任务没法执行。
json

为了解决这个问题,Javascript语言将任务的执行模式分红两种:同步(Synchronous)和异步(Asynchronous)。
segmentfault

"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,而后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则彻底不一样,每个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,因此程序的执行顺序与任务的排列顺序是不一致的、异步的
promise


异步模式"很是重要。在浏览器端,耗时很长的操做都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操做。在服务器端,"异步模式"甚至是惟一的模式,由于执行环境是单线程的,若是容许同步执行全部http请求,服务器性能会急剧降低,很快就会失去响应。浏览器

本文总结了"异步模式"编程的4种方法,理解它们可让你写出结构更合理、性能更出色、维护更方便的Javascript程序。bash

一、回调函数服务器

这是异步编程最基本的方法。异步

假定有两个函数f1和f2,后者等待前者的执行结果。async

  f1();

  f2();复制代码

若是f1是一个很耗时的任务,能够考虑改写f1,把f2写成f1的回调函数。

  function f1(callback){

    setTimeout(function () {

      // f1的任务代码

      callback();

    }, 1000);

  }复制代码

执行代码就变成下面这样:

f1(f2);复制代码

采用这种方式,咱们把同步操做变成了异步操做,f1不会堵塞程序运行,至关于先执行程序的主要逻辑,将耗时的操做推迟执行。

回调函数的优势是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而回调函数有一个致命的弱点,就是容易写出回调地狱

二、事件监听

另外一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

仍是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。

f1.on('done', f2);复制代码

上面这行代码的意思是,当f1发生done事件,就执行f2。而后,对f1进行改写:

  function f1(){

    setTimeout(function () {

      // f1的任务代码

      f1.trigger('done');

    }, 1000);

  }复制代码

f1.trigger('done')表示,执行完成后,当即触发done事件,从而开始执行f2。

这种方法的优势是比较容易理解,能够绑定多个事件,每一个事件能够指定多个回调函数,并且能够"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

二、发布/订阅

上一节的"事件",彻底能够理解成"信号"。

咱们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其余任务能够向信号中心"订阅"(subscribe)这个信号,从而知道何时本身能够开始执行。这就叫作"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

首先,f2向"信号中心"jQuery订阅"done"信号。

  jQuery.subscribe("done", f2);复制代码

而后,f1进行以下改写:

  function f1(){

    setTimeout(function () {

      // f1的任务代码

      jQuery.publish("done");

    }, 1000);

  }复制代码

jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引起f2的执行。

此外,f2完成执行后,也能够取消订阅(unsubscribe)。

jQuery.unsubscribe("done", f2);复制代码

这种方法的性质与"事件监听"相似,可是明显优于后者。由于咱们能够经过查看"消息中心",了解存在多少信号、每一个信号有多少订阅者,从而监控程序的运行

四、Promises对象

Promises对象是CommonJS工做组提出的一种规范,目的是为异步编程提供统一接口。

Promise对象有如下两个特色。

(1)对象的状态不受外界影响。Promise对象表明一个异步操做,

有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。

只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。

这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其余手段没法改变。

(2)一旦状态改变,就不会再变,任什么时候候均可以获得这个结果。

Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected

只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会当即获得这个结果。

这与事件(Event)彻底不一样,事件的特色是,若是你错过了它,再去监听,是得不到结果的。





有了Promise对象,就能够将异步操做以同步操做的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操做更加容易。

Promise也有一些缺点。首先,没法取消Promise,一旦新建它就会当即执行,没法中途取消。其次,若是不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于Pending状态时,没法得知目前进展到哪个阶段(刚刚开始仍是即将完成)。

基本用法

var promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操做成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});复制代码

Promise实例生成之后,能够用then方法分别指定Resolved状态和Reject状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});复制代码

Promise.prototype.then()

Promise实例具备then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的做用是为Promise实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。所以能够采用链式写法,即then方法后面再调用另外一个then方法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});复制代码

Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

getJSON("/posts.json").then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});复制代码

五、Generator 函数

Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数彻底不一样

Generator函数有多种理解角度。从语法上,首先能够把它理解成,Generator函数是一个状态机,封装了多个内部状态。

执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,仍是一个遍历器对象生成函数。返回的遍历器对象,能够依次遍历Generator函数内部的每个状态。

形式上,Generator函数是一个普通函数,可是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield语句,定义不一样的内部状态(yield语句在英语里的意思就是“产出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();复制代码

上面代码定义了一个Generator函数helloWorldGenerator,它内部有两个yield语句“hello”和“world”,即该函数有三个状态:hello,world和return语句(结束执行)。

而后,Generator函数的调用方法与普通函数同样,也是在函数名后面加上一对圆括号。不一样的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法能够恢复执行。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }复制代码

上面代码一共调用了四次next方法。

第一次调用,Generator函数开始执行,直到遇到第一个yield语句为止。next方法返回一个对象,它的value属性就是当前yield语句的值hello,done属性的值false,表示遍历尚未结束。

第二次调用,Generator函数从上次yield语句停下的地方,一直执行到下一个yield语句。next方法返回的对象的value属性就是当前yield语句的值world,done属性的值false,表示遍历尚未结束。

第三次调用,Generator函数从上次yield语句停下的地方,一直执行到return语句(若是没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(若是没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

第四次调用,此时Generator函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。之后再调用next方法,返回的都是这个值。

总结一下,调用Generator函数,返回一个遍历器对象,表明Generator函数的内部指针。之后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束

yield语句

因为Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,因此其实提供了一种能够暂停执行的函数。yield语句就是暂停标志。

遍历器对象的next方法的运行逻辑以下。

(1)遇到yield语句,就暂停执行后面的操做,并将紧跟在yield后面的那个表达式的值,做为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。

(3)若是没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,做为返回的对象的value属性值。

(4)若是该函数没有return语句,则返回的对象的value属性值为undefined

须要注意的是,yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,所以等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

function* gen() {
  yield  123 + 456;
}复制代码

上面代码中,yield后面的表达式123 + 456,不会当即求值,只会在next方法将指针移到这一句时,才会求值。

yield语句与return语句既有类似之处,也有区别。类似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具有位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,可是能够执行屡次(或者说多个)yield语句。正常函数只能返回一个值,由于只能执行一次return;Generator函数能够返回一系列的值,由于能够有任意多个yield。从另外一个角度看,也能够说Generator生成了一系列的值,这也就是它的名称的来历(在英语中,generator这个词是“生成器”的意思)。

六、async与await

ES7提供了async函数,使得异步操做变得更加方便。async函数是什么?一句话,async函数就是Generator函数的语法糖。

依次读取两个文件。

var fs = require('fs');

var readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};复制代码

写成async函数,就是下面这样。

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};复制代码

一比较就会发现,async函数就是将Generator函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体如今如下四点

(1)内置执行器。Generator函数的执行必须靠执行器,因此才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数如出一辙,只要一行。

(2)更好的语义。asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操做,await表示紧跟在后面的表达式须要等待结果。

(3)更广的适用性。 co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,能够是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操做)。

(4)返回值是Promise。async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了。你能够用then方法指定下一步的操做。

进一步说,async函数彻底能够看做多个异步操做,包装成的一个Promise对象,而await命令就是内部then命令的语法糖。

参考文章

JavaScript异步编程的4种方法

相关文章
相关标签/搜索