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種方法

相關文章
相關標籤/搜索