你應該知道,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
方法,就會返回一個有着value
和done
兩個屬性的對象。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)更好的語義。async
和await
,比起星號和yield
,語義更清楚了。async
表示函數裏有異步操做,await
表示緊跟在後面的表達式須要等待結果。
(3)更廣的適用性。 co
模塊約定,yield
命令後面只能是Thunk函數或Promise對象,而async
函數的await
命令後面,能夠是Promise對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。
(4)返回值是Promise。async
函數的返回值是Promise對象,這比Generator函數的返回值是Iterator對象方便多了。你能夠用then
方法指定下一步的操做。
進一步說,async
函數徹底能夠看做多個異步操做,包裝成的一個Promise對象,而await
命令就是內部then
命令的語法糖。