
上個(gè)寒假又又又···不知道第幾遍重溫ES6,以下對(duì)幾個(gè)異步實(shí)現(xiàn)寫(xiě)個(gè)總結(jié)。
在之前,我們最常見(jiàn)的異步實(shí)現(xiàn)莫過(guò)于回調(diào)函數(shù),但是如果嵌套太深,代碼就會(huì)變得難以閱讀與維護(hù)。而在ES6中,我們有了更多更加優(yōu)美的異步實(shí)現(xiàn)方式,正因如此,我們才得以走出回調(diào)地獄。
Promise
Promise是異步編程的一種優(yōu)良的解決方案,它比傳統(tǒng)的回調(diào)更加合理。在ES6中,Promise是一個(gè)對(duì)象,它有三種狀態(tài):進(jìn)行中(pending)、已成功(resolved)、已失?。╮ejected)。所以Promise可能的改變就只有兩種,從進(jìn)行中轉(zhuǎn)變到已成功或者從進(jìn)行中轉(zhuǎn)變到失敗,并且一旦狀態(tài)發(fā)生改變,之后便會(huì)一直保持這個(gè)狀態(tài),不會(huì)也無(wú)法改變。
基礎(chǔ)用法
let promise = new Promise(function(resolve, reject) {
resolve('successed');
})
promise.then(function(data) {
console.log(data);
}, function(err) {
console.log(err);
})
//'successed'
Promise的構(gòu)造函數(shù)接收一個(gè)帶有兩個(gè)參數(shù)的函數(shù),這兩個(gè)參數(shù)分別是Promise的狀態(tài)改變?yōu)槌晒褪〉暮瘮?shù),然后使用then方法分別設(shè)置狀態(tài)變?yōu)槌晒褪『蟮牟僮鳌?/p>
resolve和reject函數(shù)都可以接受一個(gè)參數(shù),這個(gè)參數(shù)傳遞會(huì)給then方法,所以上面代碼,當(dāng)resolve函數(shù)把promise的狀態(tài)變成已成功之后,會(huì)把它的參數(shù)“successed”傳遞給then方法,因此最后then方法會(huì)把這個(gè)字符串打印出來(lái)。
下面我們?cè)賹?xiě)一個(gè)讀取文件的Promise
//ES5的寫(xiě)法
var fs = require('fs');
fs.readFile('./a.txt', function(err, data) {
console.log(data.toString('utf8'));
fs.readFile('./b.txt', function(err, data) {
console.log(data.toString());
fs.readFile('./c.txt', function(err, data) {
console.log(data.toString());
});
});
})
//ES6的寫(xiě)法
function returnFilePromise(fileName) {
return promise = new Promise((resolve, reject) => {
fs.readFile(fileName, function(err, data) {
if (!err) {
resolve(data);
} else {
reject(err);
}
});
})
}
let promise = returnFilePromise('./a.txt');
promise
.then(function(data) {
console.log(data.toString());
return returnFilePromise('./b.txt');
})
.then(function (data) {
console.log(data.toString());
return returnFilePromise('./c.txt');
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
})
上面兩段代碼的功能一樣,都是繼發(fā)異步讀取三個(gè)文件,但是我們可以看到,用回調(diào)函數(shù)來(lái)寫(xiě),則會(huì)出現(xiàn)多層嵌套,使得代碼很難看,要是嵌套層數(shù)更多的話,代碼的可讀性和可維護(hù)性就會(huì)變得很差。而Promise卻可以采用鏈?zhǔn)綄?xiě)法(因?yàn)閠hen可以返回一個(gè)新的Promise),整個(gè)過(guò)程非常直觀,最后的一個(gè)catch方法可以捕捉到前面三個(gè)then方法拋出的錯(cuò)誤。
當(dāng)然,Promise的魅力遠(yuǎn)不止這些,它還有很多有趣的方法比如finally方法、all方法和race方法等等。
Generator
基礎(chǔ)用法
Generator函數(shù)是ES6中另一種異步編程方式,它會(huì)返回一個(gè)遍歷器對(duì)象。Generator有三個(gè)跟其他函數(shù)不一樣的地方:函數(shù)名帶星號(hào)(*),函數(shù)內(nèi)部的yield表達(dá)式和next調(diào)用函數(shù)方法。
function * g () {
console.log(0);
yield 1;
yield 2;
console.log(666);
yield 3;
return 4;
yield 5;
}
var ge = g();
ge.next();
ge.next();
ge.next();
ge.next();
ge.next();
//0
//{ value: 1, done: false }
//{ value: 2, done: false }
//666
//{ value: 3, done: false }
//{ value: 4, done: false }
//{ value: undefined, done: true }
由上面的代碼可以清楚地看到,我們每調(diào)用一次遍歷器對(duì)象next方法,函數(shù)就會(huì)執(zhí)行一次,直到遇到并執(zhí)行完一個(gè)yield表達(dá)式,函數(shù)就會(huì)暫停下來(lái),并保留這個(gè)狀態(tài),再調(diào)用一次next方法,函數(shù)會(huì)從上次停下來(lái)的地方接著執(zhí)行下去,直到執(zhí)行一個(gè)yield表達(dá)式后,又停下來(lái),等待下一個(gè)next方法。當(dāng)遇到return時(shí),整個(gè)函數(shù)執(zhí)行完畢,即使后面還有yield表達(dá)式也不會(huì)執(zhí)行。
next方法返回的是一個(gè)有兩個(gè)屬性的對(duì)象,第一個(gè)屬性是yield后面的值,第二個(gè)屬性是表示Generator函數(shù)是否執(zhí)行完畢,當(dāng)done屬性為true是則表示函數(shù)已經(jīng)執(zhí)行完畢了。
next方法可以帶一個(gè)參數(shù),這個(gè)參數(shù)代表上一個(gè)yield的返回值
//不帶參數(shù)
function * g () {
var a = yield 1;
console.log(a);
}
var ge = g();
console.log(ge.next())
console.log(ge.next())
//{ value: 1, done: false }
//undefined
//{ value: undefined, done: true }
//帶參數(shù)
function * g () {
var a = yield 1;
console.log(a);
}
var ge = g();
console.log(ge.next())
console.log(ge.next(1))
//{ value: 1, done: false }
//1
//{ value: undefined, done: true }
如果next不帶參數(shù)的話,那么上一個(gè)yield的返回值是undefined。
有了next帶參數(shù)這個(gè)功能,我們就可以在函數(shù)運(yùn)行之后,還能向函數(shù)體內(nèi)輸入值,用以調(diào)整函數(shù)行為。
當(dāng)yield太多時(shí),手動(dòng)next未免太麻煩,這時(shí)可以用for of循環(huán)來(lái)一次性執(zhí)行
function* g() {
yield 1;
yield 2;
yield 3;
}
for (let n of g()) {
console.log(n);
}
// 1 2 3
Generator的異步使用
先簡(jiǎn)單介紹一下協(xié)程。多個(gè)函數(shù)并行執(zhí)行,但是只能有一個(gè)函數(shù)處于運(yùn)行狀態(tài),其它函數(shù)處于暫停狀態(tài),各個(gè)函數(shù)之間可以交換函數(shù)執(zhí)行權(quán)。從內(nèi)存上看,協(xié)程有多個(gè)函數(shù)棧,但是只有一個(gè)棧處于運(yùn)行狀態(tài),這是一種以犧牲內(nèi)存來(lái)實(shí)現(xiàn)多任務(wù)并行的方法。也正是因?yàn)镚enerator可以交出和收回執(zhí)行權(quán),所以它很適合處理異步
let returnFilePromise = (fileName) => {
return new Promise((resolve, reject) => {
fs.readFile(fileName, function(err, data) {
if (!err) {
resolve(data);
} else {
reject(err);
}
})
})
}
function * readFiles () {
yield returnFilePromise('./a.txt');
yield returnFilePromise('./b.txt');
yield returnFilePromise('./c.txt');
}
let ge = readFiles();
function gogogo (fn) {
let result = fn.next()
if (!result.done) {
result.value.then(function (data) {
gogogo(fn);
console.log(data.toString())
})
}
}
gogogo(ge);
//This is A
//This is B
//This is C
上面依舊是一個(gè)讀取三個(gè)文件的異步代碼(讀取完a.txt后再讀取b.txt),用Generator寫(xiě)起來(lái)的感覺(jué)特別像是在寫(xiě)同步代碼,實(shí)際上他是異步執(zhí)行的,這里我們另外寫(xiě)了一個(gè)讓Generator可以自動(dòng)執(zhí)行的函數(shù),每個(gè)yield返回值都是一個(gè)Promise,實(shí)際上,Generator經(jīng)常會(huì)使用到Promise。
為了便于管理Generator函數(shù),除了使每個(gè)yield函數(shù)返回Promise外,我們也可以利用Thunk函數(shù)做到這一點(diǎn)。
Thunk函數(shù)
一般來(lái)說(shuō),Thunk函數(shù)是一種“傳名調(diào)用”的實(shí)現(xiàn)方法
var a = 1
function fn (n) {
console.log(n);
}
fn(a + 1); //相當(dāng)于fn(2), 參數(shù)在進(jìn)入函數(shù)體之前求值,傳值調(diào)用
//使用Thunk函數(shù)
function fn (th) {
console.log(th());
}
function thunk () {
return a + 1;
}
fn(thunk) //傳名調(diào)用
在JS中,Thunk函數(shù)會(huì)用來(lái)把一個(gè)多參數(shù)的函數(shù)變成值接受一個(gè)回調(diào)函數(shù)為參數(shù)的單參數(shù)函數(shù),任何有回調(diào)函數(shù)作參數(shù)的函數(shù)都可以寫(xiě)成Thunk函數(shù)的形式。
let thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
}
}
在實(shí)際生產(chǎn)開(kāi)發(fā)時(shí),一般會(huì)用thunkify模塊來(lái)進(jìn)行轉(zhuǎn)化,相關(guān)模塊的具體使用這里不再贅述。
async函數(shù)
async函數(shù)是Generator的語(yǔ)法糖,使得異步操作變得更加方便和語(yǔ)義化。并且它可以跟普通函數(shù)一樣調(diào)用執(zhí)行(自帶執(zhí)行器),而不需要像Generator那樣部署執(zhí)行器。
let myAsync = async function () {
let a = await returnFilePromise('./a.txt');
let b = await returnFilePromise('./b.txt');
let c = await returnFilePromise('./c.txt');
console.log(a.toString());
console.log(b.toString());
console.log(c.toString());
return c.toString();
}
myAsync().then(data => console.log(data.toString()))
//This is A
//This is B
//This is C
//This is C
await后面接的是Promise,當(dāng)然也可以是原始類型的值,但就變成了同步操作(轉(zhuǎn)化成Promise后立即執(zhí)行resolve())。整個(gè)async函數(shù)的返回值是一個(gè)Promise,因此可以在函數(shù)調(diào)用后使用then方法,函數(shù)體內(nèi)的return的返回值會(huì)當(dāng)做then的回調(diào)函數(shù)的參數(shù)。
await會(huì)按代碼書(shū)寫(xiě)的順序執(zhí)行,在上面代碼中,必須等a.txt讀取完畢,下一個(gè)await才會(huì)執(zhí)行,也就是說(shuō),在await函數(shù)體內(nèi),await表達(dá)式是同步的。但是整個(gè)async函數(shù)是異步的。如果多個(gè)異步操作之間不存在繼發(fā)關(guān)系,這樣的等待未免多余,我們可以用Promise.all方法把多個(gè)異步操作放在一起。
await的返回值是它后面Promise中resolve或者reject的參數(shù),其中reject的參數(shù)會(huì)被catch方法捕獲,不管有沒(méi)有寫(xiě)return。只要有一個(gè)await的Promise的狀態(tài)變?yōu)閞eject,那么整個(gè)async函數(shù)就會(huì)中斷執(zhí)行(async返回的Promise的狀態(tài)變?yōu)閞eject),如果我們希望有一個(gè)異步操作的成功與否不影響其他操作,那么我們可以把它放在try···catch中。
async function getErr() {
try {
await Promise.reject("error");
} catch (e) {
console.log(e);
}
return await returnFilePromise('./a.txt');
}
getErr().then(data => console.log(data.toString()))
//error
//This is A
或者稍微暴力一點(diǎn),直接在那個(gè)Promise后面接一個(gè)catch方法來(lái)捕獲錯(cuò)誤
async function getErr() {
await Promise.reject("error")
.catch(e => console.log(e));
return await returnFilePromise('./a.txt');
}
getErr().then(data => console.log(data.toString()));
最后
Promise確實(shí)是個(gè)好東西,Generator和async函數(shù)都用到了它。雖然在ES6之前就已經(jīng)有了Promise,但是直到ES6才被寫(xiě)進(jìn)標(biāo)準(zhǔn),并大放異彩。
還有一件事,我偷懶了沒(méi)好好寫(xiě)前兩部分的錯(cuò)誤處理,哈哈哈哈哈哈~