入理解異步編程的核心 Promise和Generator、Async/await 等異步編程的語法糖

其實(shí)在 ES6 標(biāo)準(zhǔn)出現(xiàn)之前,社區(qū)就最早提出了 Promise 的方案,后隨著 ES6 將其加入進(jìn)去,才統(tǒng)一了其用法,并提供了原生的 Promise 對象,Promise 也是日常前端開發(fā)使用比較多的編程方式。

Promise 的基本情況

如果一定要解釋 Promise 到底是什么,簡單來說它就是一個容器,里面保存著某個未來才會結(jié)束的事件(通常是異步操作)的結(jié)果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。

Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。我們來簡單看一下 Promise 實(shí)現(xiàn)的鏈?zhǔn)秸{(diào)用代碼,如下所示。

function read(url) {

? ? return new Promise((resolve, reject) => {

? ? ? ? fs.readFile(url, 'utf8', (err, data) => {

? ? ? ? ? ? if(err) reject(err);

? ? ? ? ? ? resolve(data);

? ? ? ? });

? ? });

}

read(A).then(data => {

? ? return read(B);

}).then(data => {

? ? return read(C);

}).then(data => {

? ? return read(D);

}).catch(reason => {

? ? console.log(reason);

});

結(jié)合上面的代碼,我們一起來分析一下 Promise 內(nèi)部的狀態(tài)流轉(zhuǎn)情況,Promise 對象在被創(chuàng)建出來時是待定的狀態(tài),它讓你能夠把異步操作返回最終的成功值或者失敗原因,和相應(yīng)的處理程序關(guān)聯(lián)起來。

一般 Promise 在執(zhí)行過程中,必然會處于以下幾種狀態(tài)之一。

1、待定(pending):初始狀態(tài),既沒有被完成,也沒有被拒絕。

2、已完成(fulfilled):操作成功完成。

3、已拒絕(rejected):操作失敗。

待定狀態(tài)的 Promise 對象執(zhí)行的話,最后要么會通過一個值完成,要么會通過一個原因被拒絕。當(dāng)其中一種情況發(fā)生時,我們用 Promise 的 then 方法排列起來的相關(guān)處理程序就會被調(diào)用。因?yàn)樽詈?Promise.prototype.then 和 Promise.prototype.catch 方法返回的是一個 Promise, 所以它們可以繼續(xù)被鏈?zhǔn)秸{(diào)用。

關(guān)于 Promise 的狀態(tài)流轉(zhuǎn)情況,有一點(diǎn)值得注意的是,內(nèi)部狀態(tài)改變之后不可逆,你需要在編程過程中加以注意。文字描述比較晦澀,我們直接通過一張圖就能很清晰地看出 Promise 內(nèi)部狀態(tài)流轉(zhuǎn)的情況,如下所示(圖片來源于網(wǎng)絡(luò))

從上圖可以看出,我們最開始創(chuàng)建一個新的 Promise 返回給 p1 ,然后開始執(zhí)行,狀態(tài)是 pending,當(dāng)執(zhí)行 resolve 之后狀態(tài)就切換為 fulfilled,執(zhí)行 reject 之后就變?yōu)?rejected 的狀態(tài)。

關(guān)于 Promise 的狀態(tài)切換如果你想深入研究,可以學(xué)習(xí)一下“有限狀態(tài)機(jī)”這個知識點(diǎn)。日常中比較常見的狀態(tài)機(jī)有很多,比如馬路上的紅綠燈。

那么,Promise 的基本情況先介紹到這里,我們再一起來分析下,Promise 如何解決回調(diào)地獄的問題。

Promise 如何解決回調(diào)地獄

首先,請你再回想一下什么是回調(diào)地獄,回調(diào)地獄有兩個主要的問題:

1、多層嵌套的問題;

2、每種任務(wù)的處理結(jié)果存在兩種可能性(成功或失?。?,那么需要在每種任務(wù)執(zhí)行結(jié)束后分別處理這兩種可能性。

這兩種問題在“回調(diào)函數(shù)時代”尤為突出,Promise 的誕生就是為了解決這兩個問題。Promise 利用了三大技術(shù)手段來解決回調(diào)地獄:回調(diào)函數(shù)延遲綁定、返回值穿透、錯誤冒泡。

下面我們通過一段代碼來說明,如下所示。

let readFilePromise = filename => {

? return new Promise((resolve, reject) => {

? ? fs.readFile(filename, (err, data) => {

? ? ? if (err) {

? ? ? ? reject(err)

? ? ? } else {

? ? ? ? resolve(data)

? ? ? }

? ? })

? })

}

readFilePromise('1.json').then(data => {

? return readFilePromise('2.json')

});

從上面的代碼中可以看到,回調(diào)函數(shù)不是直接聲明的,而是通過后面的 then 方法傳入的,即延遲傳入,這就是回調(diào)函數(shù)延遲綁定。接下來我們針對上面的代碼做一下微調(diào),如下所示。

let x = readFilePromise('1.json').then(data => {

? return readFilePromise('2.json')? //這是返回的Promise

});

x.then(/* 內(nèi)部邏輯省略 */)

我們根據(jù) then 中回調(diào)函數(shù)的傳入值創(chuàng)建不同類型的 Promise,然后把返回的 Promise 穿透到外層,以供后續(xù)的調(diào)用。這里的 x 指的就是內(nèi)部返回的 Promise,然后在 x 后面可以依次完成鏈?zhǔn)秸{(diào)用。這便是返回值穿透的效果,這兩種技術(shù)一起作用便可以將深層的嵌套回調(diào)寫成下面的形式。

readFilePromise('1.json').then(data => {

? ? return readFilePromise('2.json');

}).then(data => {

? ? return readFilePromise('3.json');

}).then(data => {

? ? return readFilePromise('4.json');

});

這樣就顯得清爽了許多,更重要的是,它更符合人的線性思維模式,開發(fā)體驗(yàn)也更好,兩種技術(shù)結(jié)合產(chǎn)生了鏈?zhǔn)秸{(diào)用的效果。

這樣解決了多層嵌套的問題,那另外一個問題,即每次任務(wù)執(zhí)行結(jié)束后分別處理成功和失敗的情況怎么解決的呢?Promise 采用了錯誤冒泡的方式。其實(shí)很容易理解,我們來看看效果。

readFilePromise('1.json').then(data => {

? ? return readFilePromise('2.json');

}).then(data => {

? ? return readFilePromise('3.json');

}).then(data => {

? ? return readFilePromise('4.json');

}).catch(err => {

? // xxx

})

這樣前面產(chǎn)生的錯誤會一直向后傳遞,被 catch 接收到,就不用頻繁地檢查錯誤了。從上面的這些代碼中可以看到,Promise 解決效果也比較明顯:實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,解決多層嵌套問題;實(shí)現(xiàn)錯誤冒泡后一站式處理,解決每次任務(wù)中判斷錯誤、增加代碼混亂度的問題。

接下來我們再看看 Promise 提供了哪些靜態(tài)的方法。

Promise 的靜態(tài)方法

我會從語法、參數(shù)以及方法的代碼幾個方面來分別介紹 all、allSettled、any、race 這四種方法。

all 方法

語法: Promise.all(iterable)

參數(shù): 一個可迭代對象,如 Array。

描述: 此方法對于匯總多個 promise 的結(jié)果很有用,在 ES6 中可以將多個 Promise.all 異步請求并行操作,返回結(jié)果一般有下面兩種情況。

1、當(dāng)所有結(jié)果成功返回時按照請求順序返回成功。

2、當(dāng)其中有一個失敗方法時,則進(jìn)入失敗方法。

我們來看下業(yè)務(wù)的場景,對于下面這個業(yè)務(wù)場景頁面的加載,將多個請求合并到一起,用 all 來實(shí)現(xiàn)可能效果會更好,請看代碼片段。

//1.獲取輪播數(shù)據(jù)列表

function getBannerList(){

? return new Promise((resolve,reject)=>{

? ? ? setTimeout(function(){

? ? ? ? resolve('輪播數(shù)據(jù)')

? ? ? },300)

? })

}

//2.獲取店鋪列表

function getStoreList(){

? return new Promise((resolve,reject)=>{

? ? setTimeout(function(){

? ? ? resolve('店鋪數(shù)據(jù)')

? ? },500)

? })

}

//3.獲取分類列表

function getCategoryList(){

? return new Promise((resolve,reject)=>{

? ? setTimeout(function(){

? ? ? resolve('分類數(shù)據(jù)')

? ? },700)

? })

}

function initLoad(){

? Promise.all([getBannerList(),getStoreList(),getCategoryList()])

? .then(res=>{

? ? console.log(res)

? }).catch(err=>{

? ? console.log(err)

? })

}

initLoad()

從上面代碼中可以看出,在一個頁面中需要加載獲取輪播列表、獲取店鋪列表、獲取分類列表這三個操作,頁面需要同時發(fā)出請求進(jìn)行頁面渲染,這樣用 Promise.all 來實(shí)現(xiàn),看起來更清晰、一目了然。

下面我們再來看另一種方法。

allSettled 方法

Promise.allSettled 的語法及參數(shù)跟 Promise.all 類似,其參數(shù)接受一個 Promise 的數(shù)組,返回一個新的 Promise。唯一的不同在于,執(zhí)行完之后不會失敗,也就是說當(dāng) Promise.allSettled 全部處理完成后,我們可以拿到每個 Promise 的狀態(tài),而不管其是否處理成功。

我們來看一下用 allSettled 實(shí)現(xiàn)的一段代碼。

const resolved = Promise.resolve(2);

const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {

? console.log(results);

});

// 返回結(jié)果:

// [

//? ? { status: 'fulfilled', value: 2 },

//? ? { status: 'rejected', reason: -1 }

// ]

從上面代碼中可以看到,Promise.allSettled 最后返回的是一個數(shù)組,記錄傳進(jìn)來的參數(shù)中每個 Promise 的返回值,這就是和 all 方法不太一樣的地方。你也可以根據(jù) all 方法提供的業(yè)務(wù)場景的代碼進(jìn)行改造,其實(shí)也能知道多個請求發(fā)出去之后,Promise 最后返回的是每個參數(shù)的最終狀態(tài)。

接下來看一下 any 這個方法。

any 方法

語法: Promise.any(iterable)

參數(shù): iterable 可迭代的對象,例如 Array。

描述: any 方法返回一個 Promise,只要參數(shù) Promise 實(shí)例有一個變成 fulfilled 狀態(tài),最后 any 返回的實(shí)例就會變成 fulfilled 狀態(tài);如果所有參數(shù) Promise 實(shí)例都變成 rejected 狀態(tài),包裝實(shí)例就會變成 rejected 狀態(tài)。

還是對上面 allSettled 這段代碼進(jìn)行改造,我們來看下改造完的代碼和執(zhí)行結(jié)果。

const resolved = Promise.resolve(2);

const rejected = Promise.reject(-1);

const anyPromise = Promise.any([resolved, rejected]);

anyPromise.then(function (results) {

? console.log(results);

});

// 返回結(jié)果:

// 2

從改造后的代碼中可以看出,只要其中一個 Promise 變成 fulfilled 狀態(tài),那么 any 最后就返回這個 Promise。由于上面 resolved 這個 Promise 已經(jīng)是 resolve 的了,故最后返回結(jié)果為 2。

我們最后來看一下 race 方法。

race 方法

語法: Promise.race(iterable)

參數(shù): iterable 可迭代的對象,例如 Array。

描述: race 方法返回一個 Promise,只要參數(shù)的 Promise 之中有一個實(shí)例率先改變狀態(tài),則 race 方法的返回狀態(tài)就跟著改變。那個率先改變的 Promise 實(shí)例的返回值,就傳遞給 race 方法的回調(diào)函數(shù)。

我們來看一下這個業(yè)務(wù)場景,對于圖片的加載,特別適合用 race 方法來解決,將圖片請求和超時判斷放到一起,用 race 來實(shí)現(xiàn)圖片的超時判斷。請看代碼片段。

//請求某個圖片資源

function requestImg(){

? var p = new Promise(function(resolve, reject){

? ? var img = new Image();

? ? img.onload = function(){ resolve(img); }

? ? img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png';

? });

? return p;

}

//延時函數(shù),用于給請求計(jì)時

function timeout(){

? var p = new Promise(function(resolve, reject){

? ? setTimeout(function(){ reject('圖片請求超時'); }, 5000);

? });

? return p;

}

Promise.race([requestImg(), timeout()])

.then(function(results){

? console.log(results);

})

.catch(function(reason){

? console.log(reason);

});

從上面的代碼中可以看出,采用 Promise 的方式來判斷圖片是否加載成功,也是針對 Promise.race 方法的一個比較好的業(yè)務(wù)場景。

綜上,這四種方法的參數(shù)傳遞形式基本是一致的,但是最后每個方法實(shí)現(xiàn)的功能還是略微有些差異的,這一點(diǎn)你需要留意。

上面探討了 JS 異步編程以及其中 Promise 的編程方式,那么下面來了解另外兩種異步編程的方式。Generator 是 ES6 標(biāo)準(zhǔn)中的異步編程方式,而 async/await 是 ES7 標(biāo)準(zhǔn)中的。

Generator 基本介紹

Generator(生成器)是 ES6 的新關(guān)鍵詞,學(xué)習(xí)起來比較晦澀難懂,那么什么是 Generator 的函數(shù)呢?通俗來講 Generator 是一個帶星號的“函數(shù)”(它并不是真正的函數(shù),下面的代碼會為你驗(yàn)證),可以配合 yield 關(guān)鍵字來暫?;蛘邎?zhí)行函數(shù)。我們來看一段使用 Generator 的代碼,如下所示。

function* gen() {

? console.log("enter");

? let a = yield 1;

? let b = yield (function () {return 2})();

? return 3;

}

var g = gen()? ? ? ? ? // 阻塞住,不會執(zhí)行任何語句

console.log(g.next())

console.log(g.next())

console.log(g.next())

console.log(g.next())

// output:

// { value: 1, done: false }

// { value: 2, done: false }

// { value: 3, done: true }

// { value: undefined, done: true }

結(jié)合上面的代碼,我們分析一下 Generator 函數(shù)的執(zhí)行情況。Generator 中配合使用 yield 關(guān)鍵詞可以控制函數(shù)執(zhí)行的順序,每當(dāng)執(zhí)行一次 next 方法,Generator 函數(shù)會執(zhí)行到下一個存在 yield 關(guān)鍵詞的位置。

總結(jié)下來,Generator 的執(zhí)行有這幾個關(guān)鍵點(diǎn)。

1、調(diào)用 gen() 后,程序會阻塞住,不會執(zhí)行任何語句。

2、調(diào)用 g.next() 后,程序繼續(xù)執(zhí)行,直到遇到 yield 關(guān)鍵詞時執(zhí)行暫停。

3、一直執(zhí)行 next 方法,最后返回一個對象,其存在兩個屬性:value 和 done。

這就是 Generator 的基本內(nèi)容,其中提到了 yield 這個關(guān)鍵詞,下面我們就來看看它的基本情況。

yield基本介紹

yield 同樣也是 ES6 的新關(guān)鍵詞,配合 Generator 執(zhí)行以及暫停。yield 關(guān)鍵詞最后返回一個迭代器對象,該對象有 value 和 done 兩個屬性,其中 done 屬性代表返回值以及是否完成。yield 配合著 Generator,再同時使用 next 方法,可以主動控制 Generator 執(zhí)行進(jìn)度。

前面說 Generator 的時候,我舉的是一個生成器函數(shù)的示例,下面我們看看多個 Generator 配合 yield 使用的情況,請看下面一段代碼。

function* gen1() {

? ? yield 1;

? ? yield* gen2();

? ? yield 4;

}

function* gen2() {

? ? yield 2;

? ? yield 3;

}

var g = gen1();

console.log(g.next())

console.log(g.next())

console.log(g.next())

console.log(g.next())

// output:

// { value: 1, done: false }

// { value: 2, done: false }

// { value: 3, done: false }

// { value: 4, done: false }

// {value: undefined, done: true}

從上面的代碼中可以看出,使用 yield 關(guān)鍵詞的話還可以配合著 Generator 函數(shù)嵌套使用,從而控制函數(shù)執(zhí)行進(jìn)度。這樣對于 Generator 的使用,以及最終函數(shù)的執(zhí)行進(jìn)度都可以很好地控制,從而形成符合你設(shè)想的執(zhí)行順序。即便 Generator 函數(shù)相互嵌套,也能通過調(diào)用 next 方法來按照進(jìn)度一步步執(zhí)行。

那么講到這里你可能會有幾個疑惑,Generator 和異步編程有什么聯(lián)系?怎么才可以把 Generator 函數(shù)按照順序一次性執(zhí)行完呢?接著往下看,你就會明白了。

thunk 函數(shù)介紹

下面我?guī)憧匆幌?thunk 函數(shù),直接說概念可能會有些晦澀,我們通過一段代碼來了解一下什么是 thunk 函數(shù),就拿判斷數(shù)據(jù)類型來舉例,代碼如下。

let isString = (obj) => {

? return Object.prototype.toString.call(obj) === '[object String]';

};

let isFunction = (obj) => {

? return Object.prototype.toString.call(obj) === '[object Function]';

};

let isArray = (obj) => {

? return Object.prototype.toString.call(obj) === '[object Array]';

};

....

可以看到,其中出現(xiàn)了非常多重復(fù)的數(shù)據(jù)類型判斷邏輯,平常業(yè)務(wù)開發(fā)中類似的重復(fù)邏輯的場景也同樣會有很多。我們將它們做一下封裝,如下所示。

let isType = (type) => {

? return (obj) => {

? ? return Object.prototype.toString.call(obj) === `[object ${type}]`;

? }

}

那么封裝了之后我們可以這么來使用,從而來減少重復(fù)的邏輯代碼,如下所示。

let isString = isType('String');

let isArray = isType('Array');

isString("123");? ? // true

isArray([1,2,3]);? // true

相應(yīng)的 isString 和 isArray 是由 isType 方法生產(chǎn)出來的函數(shù),通過上面的方式來改造代碼,明顯簡潔了不少。像 isType 這樣的函數(shù)我們稱為 thunk 函數(shù),它的基本思路都是接收一定的參數(shù),會生產(chǎn)出定制化的函數(shù),最后使用定制化的函數(shù)去完成想要實(shí)現(xiàn)的功能。

這樣的函數(shù)在 JS 的編程過程中會遇到很多,尤其是你在閱讀一些開源項(xiàng)目時,抽象度比較高的 JS 代碼往往都會采用這樣的方式。

那么請你想一下,Generator 和 thunk 函數(shù)的結(jié)合是否能為我們帶來一定的便捷性呢?

Generator 和 thunk 結(jié)合

下面我以文件操作的代碼為例,看一下 Generator 和 thunk 的結(jié)合能夠?qū)Ξ惒讲僮鳟a(chǎn)生什么樣的效果。

const readFileThunk = (filename) => {

? return (callback) => {

? ? fs.readFile(filename, callback);

? }

}

const gen = function* () {

? const data1 = yield readFileThunk('1.txt')

? console.log(data1.toString())

? const data2 = yield readFileThunk('2.txt')

? console.log(data2.toString)

}

let g = gen();

g.next().value((err, data1) => {

? g.next(data1).value((err, data2) => {

? ? g.next(data2);

? })

})

readFileThunk 就是一個 thunk 函數(shù),上面的這種編程方式就讓 Generator 和異步操作關(guān)聯(lián)起來了。上面第三段代碼執(zhí)行起來嵌套的情況還算簡單,如果任務(wù)多起來,就會產(chǎn)生很多層的嵌套,可讀性不強(qiáng),因此我們有必要把執(zhí)行的代碼封裝優(yōu)化一下,如下所示。

function run(gen){

? const next = (err, data) => {

? ? let res = gen.next(data);

? ? if(res.done) return;

? ? res.value(next);

? }

? next();

}

run(g);

改造完之后,我們可以看到 run 函數(shù)和上面的執(zhí)行效果其實(shí)是一樣的。代碼雖然只有幾行,但其包含了遞歸的過程,解決了多層嵌套的問題,并且完成了異步操作的一次性的執(zhí)行效果。這就是通過 thunk 函數(shù)完成異步操作的情況,你可以好好體會一下。

以上介紹了 Generator 和 thunk 結(jié)合的情況,其實(shí) Promise 也可以和 Generator 配合,以實(shí)現(xiàn)上面的效果,下面我們來看一下這種情況。

Generator 和 Promise 結(jié)合

還是利用上面的輸出文件的例子,對代碼進(jìn)行改造,如下所示。

// 最后包裝成 Promise 對象進(jìn)行返回

const readFilePromise = (filename) => {

? return new Promise((resolve, reject) => {

? ? fs.readFile(filename, (err, data) => {

? ? ? if(err) {

? ? ? ? reject(err);

? ? ? }else {

? ? ? ? resolve(data);

? ? ? }

? ? })

? }).then(res => res);

}

let g = gen();

// 這塊和上面 thunk 的方式一樣

const gen = function* () {

? const data1 = yield readFilePromise('1.txt')

? console.log(data1.toString())

? const data2 = yield readFilePromise('2.txt')

? console.log(data2.toString)

}

// 這塊和上面 thunk 的方式一樣

function run(gen){

? const next = (err, data) => {

? ? let res = gen.next(data);

? ? if(res.done) return;

? ? res.value.then(next);

? }

? next();

}

run(g);

從上面的代碼可以看出,thunk 函數(shù)的方式和通過 Promise 方式執(zhí)行效果本質(zhì)上是一樣的,只不過通過 Promise 的方式也可以配合 Generator 函數(shù)實(shí)現(xiàn)同樣的異步操作。希望你能參照上面 thunk 的例子,仔細(xì)體會一下遞歸調(diào)用的過程。

co 函數(shù)庫

co 函數(shù)庫是著名程序員 TJ 發(fā)布的一個小工具,用于處理 Generator 函數(shù)的自動執(zhí)行。核心原理其實(shí)就是上面講的通過和 thunk 函數(shù)以及 Promise 對象進(jìn)行配合,包裝成一個庫。它使用起來非常簡單,比如還是用上面那段代碼,第三段代碼就可以省略了,直接引用 co 函數(shù),包裝起來就可以使用了,代碼如下。

const co = require('co');

let g = gen();

co(g).then(res =>{

? console.log(res);

})

這段代碼比較簡單,幾行就完成了之前寫的遞歸的那些操作。那么為什么 co 函數(shù)庫可以自動執(zhí)行 Generator 函數(shù),它的處理原理是什么呢?

1、因?yàn)?Generator 函數(shù)就是一個異步操作的容器,它需要一種自動執(zhí)行機(jī)制,co 函數(shù)接受 Generator 函數(shù)作為參數(shù),并最后返回一個 Promise 對象。

2、在返回的 Promise 對象里面,co 先檢查參數(shù) gen 是否為 Generator 函數(shù)。如果是,就執(zhí)行該函數(shù);如果不是就返回,并將 Promise 對象的狀態(tài)改為 resolved。

3、co 將 Generator 函數(shù)的內(nèi)部指針對象的 next 方法,包裝成 onFulfilled 函數(shù)。這主要是為了能夠捕捉拋出的錯誤。

4、關(guān)鍵的是 next 函數(shù),它會反復(fù)調(diào)用自身。

關(guān)于 co 的內(nèi)部原理,你可以去 co 的源碼庫學(xué)習(xí)。代碼不是很多,也比較清晰,按照上面我所講的思路,你可以試著去理解,這對于提升你的 JavaScript 編碼能力是很有幫助的。

那么,說完了 co 函數(shù)庫,我們最后就來探究異步編程的終極解決方案:async/await。


async/await 介紹

JS 的異步編程從最開始的回調(diào)函數(shù)的方式,演化到使用 Promise 對象,再到 Generator+co 函數(shù)的方式,每次都有一些改變,但又讓人覺得不徹底,都需要理解底層運(yùn)行機(jī)制。

而 async/await 被稱為 JS 中異步終極解決方案,它既能夠像 co+Generator 一樣用同步的方式來書寫異步代碼,又得到底層的語法支持,無須借助任何第三方庫。

接下來,我們就從原理的角度來看看 async/await 這個語法糖背后到底做了哪些優(yōu)化和改進(jìn),使得我們用起來會更加方便。還是按照上面 Generator 和 Promise 結(jié)合的例子,使用 async/await 語法糖來進(jìn)行改造,請看改造后的代碼。

// readFilePromise 依舊返回 Promise 對象

const readFilePromise = (filename) => {

? return new Promise((resolve, reject) => {

? ? fs.readFile(filename, (err, data) => {

? ? ? if(err) {

? ? ? ? reject(err);

? ? ? }else {

? ? ? ? resolve(data);

? ? ? }

? ? })

? }).then(res => res);

}

// 這里把 Generator的 * 換成 async,把 yield 換成 await

const gen = async function() {

? const data1 = await readFilePromise('1.txt')

? console.log(data1.toString())

? const data2 = await readFilePromise('2.txt')

? console.log(data2.toString)

}

從上面的代碼中可以看到,雖然我們簡單地將 Generator 的 * 號換成了 async,把 yield 換成了 await,但其實(shí) async 的內(nèi)部做了不少工作。我們根據(jù) async 的原理詳細(xì)拆解一下,看看它到底做了哪些工作。

總結(jié)下來,async 函數(shù)對 Generator 函數(shù)的改進(jìn),主要體現(xiàn)在以下三點(diǎn)。

1、內(nèi)置執(zhí)行器:Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,因?yàn)椴荒芤淮涡詧?zhí)行完成,所以之后才有了開源的 co 函數(shù)庫。但是,async 函數(shù)和正常的函數(shù)一樣執(zhí)行,也不用 co 函數(shù)庫,也不用使用 next 方法,而 async 函數(shù)自帶執(zhí)行器,會自動執(zhí)行。

2、適用性更好:co 函數(shù)庫有條件約束,yield 命令后面只能是 Thunk 函數(shù)或 Promise 對象,但是 async 函數(shù)的 await 關(guān)鍵詞后面,可以不受約束。

3、可讀性更好:async 和 await,比起使用 * 號和 yield,語義更清晰明了。

說了這么多優(yōu)點(diǎn),我們還是通過一段簡單的代碼來看下 async 返回的結(jié)果,是不是使用起來更方便,請看下面的代碼。

async function func() {

? return 100;

}

console.log(func());

// Promise {<fulfilled>: 100}

從執(zhí)行的結(jié)果可以看出,async 函數(shù) func 最后返回的結(jié)果直接是 Promise 對象,比較方便讓開發(fā)者繼續(xù)往后處理。而之前 Generator 并不會自動執(zhí)行,需要通過 next 方法控制,最后返回的也并不是 Promise 對象,而是需要通過 co 函數(shù)庫來實(shí)現(xiàn)最后返回 Promise 對象。

這樣看來,ES7 加入的 async/await 的確解決了之前的問題,使開發(fā)者在編程過程中更容易理解,語法更清晰,并且也不用再單獨(dú)引用 co 函數(shù)庫了。因此用 async/await 寫出的代碼也更加優(yōu)雅,相比于之前的 Promise 和 co+Generator 的方式更容易理解,上手成本也更低,不愧是 JS 異步的終極解決方案。

總結(jié)

最后,整理了一下 Promise 的幾個方法到下面的表格,還整理了這幾個異步編程的特點(diǎn),你可以對比著來回顧,以加深記憶,請看下面的表格。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容