衆所周知Javascript
是「單線程」語言,在實際開發中咱們又不得不面臨異步邏輯的處理,這時候異步編程就變得十分必要。所謂異步,就是指在執行一件任務,這件任務分A、B兩個階段,執行完A階段後,須要去作另一個任務獲得結果後才能執行B階段。異步編程有如下幾種經常使用方式:callback
、Promise
、Generator
、async
。html
callback函數是指經過函數傳參
傳遞到其餘執行代碼的,某一塊可執行代碼的引用,被主函數調用後又回到主函數,以下例:程序員
function add(a, b, callback){
var num = a + b;
callback(num)
}
add(1, 2, function(num){
console.log(num); # 3
# ...
})
複製代碼
若是是有個任務隊列,裏面包含多個任務的話,那就須要層層嵌套了es6
var readFile = require('fs-readfile-promise'); # 讀取文件函數
readFile(fileA, function(data) {
readFile(fileB, function(data) {
# ...
})
})
複製代碼
如上若是我存在n個任務,那須要層層嵌套n層,這樣代碼顯得很是冗餘龐雜而且耦合度很高,修改其中某一個函數的話,會影響上下函數代碼塊的邏輯。這種狀況被稱爲「回調地獄」(callback hell)
編程
Promise是咱們經常使用來解決異步回調問題的方法。容許將回調函數的嵌套,改成鏈式調用。以上多個任務的話,能夠改形成以下例子:json
function add(a, b){
return new Promise((resolve, reject) => {
var result = a+b;
resolve(result);
})
}
add(10, 20).then(res => {
return add(res, 20) # res = 30
}).then(res => {
return add(res, 20) # res = 50
}).then(res => {
// ...
}).catch(err => {
// 錯誤處理
})
複製代碼
add函數執行後會返回一個Promise
,它的結果會進入then方法中,第一個參數是Promise
的resolve
結果,第二個參數(可選)是Promise
的reject
結果。咱們能夠把回調後的邏輯在then
方法中寫,這樣的鏈式寫法有效的將各個事件的回調處理分割開來,使得代碼結構更加清晰。另外咱們能夠在catch
中處理報錯。數組
若是是咱們的異步請求不是按照順序A->B->C->D這種,而是[A,B,C]->D,先並行執行A、B、C完而後在執行D,咱們能夠用Promise.all();promise
# 生成一個Promise對象的數組
const promises = [2, 3, 5].map(function (id) {
return getJSON('/post/' + id + ".json"); # getJSON 是返回被Promise包裝的數據請求函數
});
Promise.all(promises).then(function (posts) {
# promises裏面裝了三個Promise
# posts返回的是一個數組,對應三個Promise的返回數據
# 在這能夠執行D任務
}).then(res => {
//...
}).catch(function(reason){
//...
});
複製代碼
可是Promise
的代碼仍是有些多餘的代碼,好比被Promise
包裝的函數有一堆new Promise
、then
、catch
。bash
Generator函數是ES6提供的一種異步編程解決方案,由每執行一次函數返回的是一個遍歷器對象,返回的對象能夠依次遍歷Generator裏面的每一個狀態,咱們須要用遍歷器對象的next
方法來執行函數。併發
先來個例子:異步
function* foo() {
yield 'stepone';
yield 'steptwo';
return 'stepthree';
}
var _foo = foo();
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
複製代碼
Generator有三個特徵:函數命名時function
後面須要加*
;函數內部有yield
;外部執行須要調用next
方法。每一個yield會將跟在她後面的值包裹成一個對象的返回,返回的對象中包括返回值和函數運行狀態,直到return
,返回done
爲true
。
若是每次運行Generator函數咱們都須要用next的話,你那就太麻煩了,咱們須要一個能夠自動執行器。co 模塊是著名程序員 TJ Holowaychuk 於 2013 年 6 月發佈的一個小工具,用於 Generator 函數的自動執行。 運用co模塊時,yield後面只能是 Thunk函數 或者Promise對象,co函數執行完成以後返回的是Promise。以下:
var co = require('co');
var gen = function* () {
var img1 = yield getImage('/image01');
var img2 = yield getImage('/image02');
...
};
co(gen).then(function (res){
console.log(res);
}).catch(err){
# 錯誤處理
};
複製代碼
co模塊的任務的並行處理,等多個任務並行執行完成以後再進行下一步操做:
# 數組的寫法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).then(console.log).catch(onerror);
# 對象的寫法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).then(console.log).catch(onerror);
複製代碼
Generator
函數雖然相比Promise
在寫法上更加精簡且邏輯清晰,可是須要額外有個運行co
函數去執行,爲了解決優化這個問題,async
函數出現了。
async函數是Generator
函數的語法糖。
var co = require('co');
var gen = function* () {
var img1 = yield getImage('/image01');
var img2 = yield getImage('/image02');
...
};
co(gen).then(function (res){
console.log(res);
}).catch(err){
# 錯誤處理
};
****
#以上Generator函數能夠改成
var gen = async function () {
var img1 = await getImage('/image01');
var img2 = await getImage('/image02');
return [img1, img2];
...
};
gen().then(res => {
console.log(res) # [img1, img2]
});
複製代碼
相比Generator
函數,async
函數在寫法上的區別就是async
替代了*
,await
替代了yield
,而且async
自帶執行器,只需gen()便可執行函數;擁有比較好的適應性,await
後面能夠是Promise
也能夠是原始類型的值;此外async
函數返回的是Promise
,便於咱們更好的處理返回值。
async function gen() {
return '111';
# 等同於 return await '111';
};
gen().then(res => {
console.log(res) # 111
});
複製代碼
若是是直接return值,這個值會自動成爲then方法回調函數中的值。
async function gen() {
var a = await getA();
var b = await getB();
return a + b;
};
gen().then(res => {
console.log(res)
});
複製代碼
async
函數返回的Promise
,必須等到函數體內全部await
後面的Promise
對象都執行完畢後,或者return
或者拋錯
以後才能改變狀態;也就是隻有async
裏面的異步操做所有操做完,才能回到主任務來,而且在then
方法裏面繼續執行主任務。
# 錯誤處理1
async function gen() {
await new Promise((resolve, reject) => {
throw new Error('出錯了');
})
};
gen().then(res => {
console.log(res)
}).catch(err => {
console.log(err) # 出錯了
});
# 錯誤處理2:以下處理,一個await任務的錯誤不會影響到後面await任務的執行
async function gen() {
try{
await new Promise((resolve, reject) => {
throw new Error('出錯了');
})
}catch(e){
console.log(e); # 出錯了
}
return Promise.resolve(1);
};
gen().then(res => {
console.log(res) # 1
});
複製代碼
錯誤處理如上。
async function gen() {
# 寫法一
let result = await Promise.all([getName(), getAddress()]);
return result;
# 寫法二
let namePromise = getName();
let addressPromise = getAddress();
let name = await namePromise;
let address = await addressPromise;
return [name, address];
};
gen().then(res => {
console.log(res); # 一個數組,分別是getName和getAddress返回值
})
複製代碼
多個異步任務互相沒有依賴關係,須要併發時,可按照如上兩種方法書寫。
function chainAnimationsPromise(elem, animations) {
# 變量ret用來保存上一個動畫的返回值
let ret = null;
# 新建一個空的Promise
let p = Promise.resolve();
# 使用then方法,添加全部動畫
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
# 返回一個部署了錯誤捕捉機制的Promise
return p.catch(function(e) {
# 錯誤處理
}).then(function() {
return ret;
});
}
複製代碼
Promise
雖然很好的解決了地獄回調的問題,可是代碼中有不少與語義無關的then
、catch
等;
function chainAnimationsGenerator(elem, animations) {
return co(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
# 錯誤處理
}
return ret;
});
}
複製代碼
Generator
函數須要自動執行器來執行函數,且yield
後面只能是Promise
對象或者Thunk
函數。
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
# 錯誤處理
}
return ret;
}
複製代碼
async
函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。與Generator
相比不須要程序員再提供一個執行器,async
自己自動執行,使用起來方便簡潔。