我們都知道async/await
是Generator
函數(shù)的語(yǔ)法糖,為了更加深刻的了解async/await
的原理,我們先來(lái)研究一下Generator
的相關(guān)的知識(shí)抒痒。
基本概念
Generator
:可以被認(rèn)為是一個(gè)狀態(tài)機(jī)杉允,其內(nèi)部封裝了多個(gè)內(nèi)部的狀態(tài)膳犹。執(zhí)行該函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象终蒂,也就是蜂林,Generator
函數(shù)除了狀態(tài),還是一個(gè)遍歷器對(duì)象生成函數(shù)后豫,返回的遍歷器對(duì)象悉尾,可以依次遍歷Generator
內(nèi)部的每一個(gè)狀態(tài)突那。
函數(shù)特征:
-
function
關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào) - 函數(shù)內(nèi)部使用
yield
表達(dá)式挫酿,定義不同的內(nèi)部狀態(tài)
function* foo(){
yield 'hello';
yield 'word';
return 'ending';
}
var hw = foo();
上面的代碼定義了一個(gè)Generator
函數(shù),內(nèi)部由兩個(gè)yield
表達(dá)式(hello
和world
),即將執(zhí)行的函數(shù)有三個(gè)狀態(tài):hello
,world
和return
語(yǔ)句愕难。
Generator
函數(shù)調(diào)用和普通函數(shù)的調(diào)用是一樣的早龟,也是在函數(shù)名后面加上一對(duì)圓括號(hào)。但是調(diào)用Generator
調(diào)用之后并不執(zhí)行猫缭,返回的也不是函數(shù)運(yùn)行結(jié)果葱弟,而是指向內(nèi)部狀態(tài)的指針對(duì)象,也就是上面說(shuō)的遍歷器對(duì)象猜丹。下一步必須調(diào)用遍歷對(duì)象的next
方法芝加,讓指針指向下一個(gè)狀態(tài),也就是說(shuō)射窒,每一次調(diào)用next
方法藏杖,內(nèi)部指針就會(huì)從函數(shù)頭或是上一次停下來(lái)的地方開始執(zhí)行将塑,直到遇到下一個(gè)yield
表達(dá)式為止。換言之蝌麸,Generator
函數(shù)是分段執(zhí)行的点寥,yield
表達(dá)式是暫停執(zhí)行的標(biāo)記,而next
方法可以恢復(fù)執(zhí)行来吩。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
done
屬性的值false
敢辩,表示遍歷還沒有結(jié)束。否則表示遍歷已經(jīng)結(jié)束啦弟疆。
第四次調(diào)用戚长,此時(shí) Generator
函數(shù)已經(jīng)運(yùn)行完畢,next
方法返回對(duì)象的value
屬性為undefined
兽间,done
屬性為true
历葛。以后再調(diào)用next
方法,返回的都是這個(gè)值嘀略。
在這個(gè)過(guò)程中恤溶,最重要的是next
的方法的運(yùn)行邏輯夫植,下面我們對(duì)這個(gè)過(guò)程進(jìn)行一下梳理:
- 遇到
yield
表達(dá)式杀饵,先暫停后面的操作,將緊跟著yield
后面的表達(dá)式的值箫柳,作為返回對(duì)象的value
屬性值進(jìn)行返回 - 下一次調(diào)用
next
方法的時(shí)候讼育,再繼續(xù)往下執(zhí)行帐姻,知道遇到下一個(gè)yield
表達(dá)式。 - 如果沒有遇到新的
yield
表達(dá)式奶段,就一直運(yùn)行直到函數(shù)結(jié)束饥瓷,直到return
語(yǔ)句為止,并將return
語(yǔ)句后面的表達(dá)式的值痹籍,作為返回的對(duì)象的value
屬性值呢铆。 - 如果沒有遇到
return
語(yǔ)句,就會(huì)返回對(duì)象的value
屬性值為undefined
蹲缠。
yield
表達(dá)式與return
語(yǔ)句既有相似之處棺克,也有區(qū)別。相似之處在于线定,都能返回緊跟在語(yǔ)句后面的那個(gè)表達(dá)式的值娜谊。區(qū)別在于每次遇到yield
,函數(shù)暫停執(zhí)行斤讥,下一次再?gòu)脑撐恢美^續(xù)向后執(zhí)行纱皆,而return
語(yǔ)句不具備位置記憶的功能。一個(gè)函數(shù)里面,只能執(zhí)行一次(或者說(shuō)一個(gè))return
語(yǔ)句派草,但是可以執(zhí)行多次(或者說(shuō)多個(gè))yield
表達(dá)式撑帖。
異步操作的同步化表達(dá)
我們舉一個(gè)例子來(lái)實(shí)現(xiàn)將異步的操作同步化:過(guò) Generator
函數(shù)部署 Ajax 操作,可以用同步的方式表達(dá)澳眷。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
makeAjaxCall
函數(shù)中的next
方法胡嘿,必須加上response
參數(shù),因?yàn)?code>yield表達(dá)式钳踊,本身是沒有值的衷敌,總是等于undefined
。
Generator 函數(shù)的異步應(yīng)用
ES6誕生以來(lái)主要實(shí)現(xiàn)異步編程的方法有如下的四種:
- 回調(diào)函數(shù) -- 回調(diào)地獄
- 事件監(jiān)聽
- 發(fā)布訂閱
- Promise 對(duì)象 -- 代碼冗余
回調(diào)函數(shù)
JavaScript
語(yǔ)言對(duì)異步編程的實(shí)現(xiàn)拓瞪,就是回調(diào)函數(shù)缴罗,回調(diào)函數(shù),就是把任務(wù)的第二階段單獨(dú)寫在一個(gè)函數(shù)中祭埂,等待重新執(zhí)行這個(gè)任務(wù)的時(shí)候面氓,就直接調(diào)用這個(gè)函數(shù),回調(diào)函數(shù)的英文名字就是callback
,直接翻譯過(guò)來(lái)就是"重新調(diào)用"
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
readFile
函數(shù)的第三個(gè)參數(shù)蛆橡,就是回調(diào)函數(shù)舌界,也就是任務(wù)的第二段。等到操作系統(tǒng)返回了/etc/passwd
這個(gè)文件以后泰演,回調(diào)函數(shù)才會(huì)執(zhí)行呻拌。
Promise
回調(diào)函數(shù)本身并沒有問題,它的問題出現(xiàn)在多個(gè)回調(diào)函數(shù)嵌套睦焕。假定讀取A
文件之后藐握,再讀取B
文件,代碼如下垃喊。
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
會(huì)出現(xiàn)多重回調(diào)的情況猾普,進(jìn)而造成代碼的可讀性變差,形成回調(diào)地獄('callback hell')
Promise
對(duì)象就是為了解決這個(gè)問題而提出的本谜。它不是新的語(yǔ)法功能初家,而是一種新的寫法,允許將回調(diào)函數(shù)的嵌套耕突,改成鏈?zhǔn)秸{(diào)用笤成。采用 Promise
评架,連續(xù)讀取多個(gè)文件眷茁,
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
Promise
的最大問題是代碼冗余,原來(lái)的任務(wù)被Promise
包裝了一下纵诞,不管什么操作上祈,一眼看去都是一堆then
,原來(lái)的語(yǔ)義變得很不清楚。
Generator 函數(shù)
協(xié)程有點(diǎn)像函數(shù)登刺,又有點(diǎn)像線程籽腕。它的運(yùn)行流程大致如下。
- 協(xié)程
A
開始執(zhí)行纸俭。 - 協(xié)程
A
執(zhí)行到一半皇耗,進(jìn)入暫停,執(zhí)行權(quán)轉(zhuǎn)移到協(xié)程B
揍很。 - (一段時(shí)間后)協(xié)程B交還執(zhí)行權(quán)郎楼。
- 協(xié)程
A
恢復(fù)執(zhí)行。
Generator
函數(shù)是協(xié)程在ES6
的實(shí)現(xiàn)窒悔,最大特點(diǎn)就是可以交出函數(shù)的執(zhí)行權(quán)呜袁,下面看看如何使用 Generator
函數(shù),執(zhí)行一個(gè)真實(shí)的異步任務(wù)简珠。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
async/await
前文有一個(gè)Generator
函數(shù)阶界,依次讀取兩個(gè)文件。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
上面代碼的函數(shù)gen
可以寫成async
函數(shù)聋庵,就是下面這樣膘融。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async
函數(shù)就是將Generator
函數(shù)的星號(hào)(*)
替換成async
,將yield
替換成await
async
函數(shù)對(duì) Generator
函數(shù)的改進(jìn)
- 內(nèi)置執(zhí)行器祭玉。
而async
函數(shù)自帶執(zhí)行器托启。也就是說(shuō),async
函數(shù)的執(zhí)行攘宙,與普通函數(shù)一模一樣屯耸,只要一行。 - 更好的語(yǔ)義
async
和await
蹭劈,比起星號(hào)和yield
疗绣,語(yǔ)義更清楚了。async
表示函數(shù)里有異步操作铺韧,await
表示緊跟在后面的表達(dá)式需要等待結(jié)果多矮。 - 更廣的適用性。
yield
命令后面只能是Thunk
函數(shù)或Promise
對(duì)象哈打,而async
函數(shù)的await
命令后面塔逃,可以是Promise
對(duì)象和原始類型的值(數(shù)值、字符串和布爾值料仗,但這時(shí)會(huì)自動(dòng)轉(zhuǎn)成立即resolved
的Promise
對(duì)象)湾盗。 - 返回值是
Promise
。
async
函數(shù)的返回值是Promise
對(duì)象
async 函數(shù)的實(shí)現(xiàn)原理
async
函數(shù)的實(shí)現(xiàn)原理立轧,就是將 Generator
函數(shù)和自動(dòng)執(zhí)行器格粪,包裝在一個(gè)函數(shù)里躏吊。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async
函數(shù)都可以寫成上面的第二種形式,其中的spawn
函數(shù)就是自動(dòng)執(zhí)行器,我們看一下spawn
的執(zhí)行過(guò)程:
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}