前言
眾所周知Javascript
是“單執行緒”語言,在實際開發中我們又不得不面臨非同步邏輯的處理,這時候非同步程式設計就變得十分必要。所謂非同步,就是指在執行一件任務,這件任務分A、B兩個階段,執行完A階段後,需要去做另外一個任務得到結果後才能執行B階段。非同步程式設計有以下幾種常用方式:callback
、Promise
、Generator
、async
。
callback函式
callback函式是指通過函式傳參
傳遞到其他執行程式碼的,某一塊可執行程式碼的引用,被主函式呼叫後又回到主函式,如下例:
function add(a, b, callback){
var num = a + b;
callback(num)
}
add(1, 2, function(num){
console.log(num); # 3
# ...
})
複製程式碼
如果是有個任務佇列,裡面包含多個任務的話,那就需要層層巢狀了
var readFile = require('fs-readfile-promise'); # 讀取檔案函式
readFile(fileA, function(data) {
readFile(fileB, function(data) {
# ...
})
})
複製程式碼
如上如果我存在n個任務,那需要層層巢狀n層,這樣程式碼顯得非常冗餘龐雜並且耦合度很高,修改其中某一個函式的話,會影響上下函式程式碼塊的邏輯。這種情況被稱為“回撥地獄”(callback hell)
Promise
Promise是我們常用來解決非同步回撥問題的方法。允許將回撥函式的巢狀,改為鏈式呼叫。以上多個任務的話,可以改造成如下例子:
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物件的陣列
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
。
Generator函式
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函式
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返回值
})
複製程式碼
多個非同步任務互相沒有依賴關係,需要併發時,可按照如上兩種方法書寫。
async與Promise、Generator函式之間的對比
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
本身自動執行,使用起來方便簡潔。