面試官問(wèn) async、await 函數(shù)原理是在問(wèn)什么?
1. 前言
這周看的是 co 的源碼,我對(duì) co 比較陌生,沒(méi)有了解和使用過(guò)。因此在看源碼之前,我希望能大概了解 co 是什么,解決了什么問(wèn)題。
2. 簡(jiǎn)單了解 co
先看了 co 的 GitHub,README 是這樣介紹的:
Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way.
看起來(lái)有點(diǎn)懵逼,又查了一些資料,大多說(shuō) co 是用于 generator 函數(shù)的自動(dòng)執(zhí)行。generator 是 ES6 提供的一種異步編程解決方案,它最大的特點(diǎn)是可以控制函數(shù)的執(zhí)行。
2.1 關(guān)于 generator
說(shuō)到異步編程,我們很容易想到還有 promise,async 和 await。它們有什么區(qū)別呢?先看看 JS 異步編程進(jìn)化史:callback -> promise -> generator -> async + await
JS 異步編程
再看看它們語(yǔ)法上的差異:
| Callback | Promise | Generator | async + await + Promise | 
|---|---|---|---|
| ajax(url, () => {}) | Promise((resolve,reject) => { resolve() }).then() | function* gen() { yield 1} | async getData() { await fetchData() } | 
關(guān)于 generator 的學(xué)習(xí)不在此篇幅詳寫(xiě)了,需要了解它的概念和語(yǔ)法。
3. 學(xué)習(xí)目標(biāo)
經(jīng)過(guò)簡(jiǎn)單學(xué)習(xí),大概明白了 co 產(chǎn)生的背景,因?yàn)?generator 函數(shù)不會(huì)自動(dòng)執(zhí)行,需要手動(dòng)調(diào)用它的 next() 函數(shù),co 的作用就是自動(dòng)執(zhí)行 generator 的 next() 函數(shù),直到 done 的狀態(tài)變成 true 為止。
那么我這一期的學(xué)習(xí)目標(biāo):
1)解讀 co 源碼,理解它是如何實(shí)現(xiàn)自動(dòng)執(zhí)行 generator
2)動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)略版的 co
4. 解讀 co 源碼
co 源碼地址:https://github.com/tj/co
4.1 整體架構(gòu)
從 README 中,可以看到是如何使用 co :
- co(function* () {
 - var result = yield Promise.resolve(true);
 - return result;
 - }).then(function (value) {
 - console.log(value);
 - }, function (err) {
 - console.error(err.stack);
 - });
 
從代碼可以看到它接收了一個(gè) generator 函數(shù),返回了一個(gè) Promise,這部分對(duì)應(yīng)的源碼如下。
- function co(gen) {
 - var ctx = this;
 - // 獲取參數(shù)
 - var args = slice.call(arguments, 1);
 - // 返回一個(gè) Promise
 - return new Promise(function(resolve, reject) {
 - // 把 ctx 和參數(shù)傳遞給 gen 函數(shù)
 - if (typeof gen === 'function') gengen = gen.apply(ctx, args);
 - // 判斷 gen.next 是否函數(shù),如果不是直接 resolve(gen)
 - if (!gen || typeof gen.next !== 'function') return resolve(gen);
 - // 先執(zhí)行一次 next
 - onFulfilled();
 - // 實(shí)際上就是執(zhí)行 gen.next 函數(shù),獲取 gen 的值
 - function onFulfilled(res) {
 - var ret;
 - try {
 - ret = gen.next(res);
 - } catch (e) {
 - return reject(e);
 - }
 - next(ret);
 - return null;
 - }
 - // 對(duì) gen.throw 的處理
 - function onRejected(err) {
 - var ret;
 - try {
 - ret = gen.throw(err);
 - } catch (e) {
 - return reject(e);
 - }
 - next(ret);
 - }
 - // 實(shí)際處理的函數(shù),會(huì)遞歸執(zhí)行,直到 ret.done 狀態(tài)為 true
 - function next(ret) {
 - // 如果生成器的狀態(tài) done 為 true,就 resolve(ret.value),返回結(jié)果
 - if (ret.done) return resolve(ret.value);
 - // 否則,將 gen 的結(jié)果 value 封裝成 Promise
 - var value = toPromise.call(ctx, ret.value);
 - // 判斷 value 是否 Promise,如果是就返回 then
 - if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
 - // 如果不是 Promise,Rejected
 - return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
 - + 'but the following object was passed: "' + String(ret.value) + '"'));
 - }
 - });
 - }
 
看到這里,我產(chǎn)生了一個(gè)疑問(wèn):Promise + then 也可以處理異步編程,為什么 co 的源碼里要把 Promise + generator 結(jié)合起來(lái)呢,為什么要這樣做?直到我搞懂了 co 的核心目的,它使 generator 和 yield 的語(yǔ)法更趨向于同步編程的寫(xiě)法,引用阮一峰的網(wǎng)絡(luò)日志中的一句話(huà)就是:
異步編程的語(yǔ)法目標(biāo),就是怎樣讓它更像同步編程。
可以看一個(gè) Promise + then 的例子:
- function getData() {
 - return new Promise(function(resolve, reject) {
 - resolve(1111)
 - })
 - }
 - getData().then(function(res) {
 - // 處理第一個(gè)異步的結(jié)果
 - console.log(res);
 - // 返回第二個(gè)異步
 - return Promise.resolve(2222)
 - })
 - .then(function(res) {
 - // 處理第二個(gè)異步的結(jié)果
 - console.log(res)
 - })
 - .catch(function(err) {
 - console.error(err);
 - })
 
如果有多個(gè)異步處理就會(huì)需要寫(xiě)多少個(gè) then 來(lái)處理異步之間可能存在的同步關(guān)系,從以上的代碼可以看到 then 的處理是一層一層的嵌套。如果換成 co,在寫(xiě)法上更優(yōu)雅也更符合日常同步編程的寫(xiě)法:
- co(function* () {
 - try {
 - var result1 = yield Promise.resolve(1111)
 - // 處理第一個(gè)異步的結(jié)果
 - console.log(result1);
 - // 返回第二個(gè)異步
 - var result2 = yield Promise.resolve(2222)
 - // 處理第二個(gè)異步的結(jié)果
 - console.log(result2)
 - } catch (err) {
 - console.error(err)
 - }
 - });
 
4.2 分析 next 函數(shù)
源碼的 next 函數(shù)接收一個(gè) gen.next() 返回的對(duì)象 ret 作為參數(shù),形如{value: T, done: boolean},next 函數(shù)只有四行代碼。
第一行:if (ret.done) return resolve(ret.value); 如果 ret.done 為 true,表明 gen 函數(shù)到了結(jié)束狀態(tài),就 resolve(ret.value),返回結(jié)果。
第二行:var value = toPromise.call(ctx, ret.value); 調(diào)用 toPromise.call(ctx, ret.value) 函數(shù),toPromise 函數(shù)的作用是把 ret.value 轉(zhuǎn)化成 Promise 類(lèi)型,也就是用 Promise 包裹一層再 return 出去。
- function toPromise(obj) {
 - // 如果 obj 不存在,直接返回 obj
 - if (!obj) return obj;
 - // 如果 obj 是 Promise 類(lèi)型,直接返回 obj
 - if (isPromise(obj)) return obj;
 - // 如果 obj 是生成器函數(shù)或遍歷器對(duì)象, 就遞歸調(diào)用 co 函數(shù)
 - if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
 - // 如果 obj 是普通的函數(shù)類(lèi)型,轉(zhuǎn)換成 Promise 類(lèi)型函數(shù)再返回
 - if ('function' == typeof obj) return thunkToPromise.call(this, obj);
 - // 如果 obj 是一個(gè)數(shù)組, 轉(zhuǎn)換成 Promise 數(shù)組再返回
 - if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
 - // 如果 obj 是一個(gè)對(duì)象, 轉(zhuǎn)換成 Promise 對(duì)象再返回
 - if (isObject(obj)) return objectToPromise.call(this, obj);
 - // 其他情況直接返回
 - return obj;
 - }
 
第三行:if (value && isPromise(value)) return value.then(onFulfilled, onRejected); 如果 value 是 Promise 類(lèi)型,調(diào)用 onFulfilled 或 onRejected,實(shí)際上是遞歸調(diào)用了 next 函數(shù)本身,直到 done 狀態(tài)為 true 或 throw error。
第四行:return onRejected(...) 如果不是 Promise,直接 Rejected。
5. 實(shí)踐
雖然解讀了 co 的核心代碼,看起來(lái)像是懂了,實(shí)際上很容易遺忘。為了加深理解,結(jié)合上面的 co 源碼和自己的思路動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)略版的 co。
5.1 模擬請(qǐng)求
- function request() {
 - return new Promise((resolve) => {
 - setTimeout(() => {
 - resolve({data: 'request'});
 - }, 1000);
 - });
 - }
 - // 用 yield 獲取 request 的值
 - function* getData() {
 - yield request()
 - }
 - var g = getData()
 - var {value, done} = g.next()
 - // 間隔1s后打印 {data: "request"}
 - value.then(res => console.log(res))
 
5.2 模擬實(shí)現(xiàn)簡(jiǎn)版 co
核心實(shí)現(xiàn):
1)函數(shù)傳參
2)generator.next 自動(dòng)執(zhí)行
- function co(gen) {
 - // 1. 傳參
 - var ctx = this;
 - const args = Array.prototype.slice.call(arguments, 1);
 - gengen = gen.apply(ctx, args);
 - return new Promise(function(resolve, reject) {
 - // 2. 自動(dòng)執(zhí)行 next
 - onFulfilled()
 - function onFulfilled (res) {
 - var ret = gen.next(res);
 - next(ret);
 - }
 - function next(ret){
 - if (ret.done) return resolve(ret.value);
 - // 此處只處理 ret.value 是 Promise 對(duì)象的情況,其他類(lèi)型簡(jiǎn)略版沒(méi)處理
 - var promise = ret.value;
 - // 自動(dòng)執(zhí)行
 - promise && promise.then(onFulfilled);
 - }
 - })
 - }
 - // 執(zhí)行
 - co(function* getData() {
 - var result = yield request();
 - // 1s后打印 {data: "request"}
 - console.log(result)
 - })
 
6. 感想
學(xué)習(xí)一個(gè)新的東西(generator)花費(fèi)的時(shí)間遠(yuǎn)遠(yuǎn)大于單純閱讀源碼的時(shí)間,因?yàn)樾枰私馑a(chǎn)生的背景,語(yǔ)法,解決的問(wèn)題以及一些應(yīng)用場(chǎng)景,這樣在閱讀源碼的時(shí)候才知道它為什么要這樣寫(xiě)。
讀完源碼,我們會(huì)發(fā)現(xiàn),其實(shí) co 就是一個(gè)自動(dòng)執(zhí)行 next() 的函數(shù),而且到最后我們會(huì)發(fā)現(xiàn) co 的寫(xiě)法和我們?nèi)粘J褂玫?async/await 的寫(xiě)法非常相像,因此也不難理解【async/await 實(shí)際上是對(duì) generator 封裝的一個(gè)語(yǔ)法糖】這句話(huà)了。
- // co 寫(xiě)法
 - co(function* getData() {
 - var result = yield request();
 - // 1s后打印 {data: "request"}
 - console.log(result)
 - })
 - // async await 寫(xiě)法
 - (async function getData() {
 - var result = await request();
 - // 1s后打印 {data: "request"}
 - console.log(result)
 - })()
 
不得不說(shuō),閱讀源碼的確是一個(gè)開(kāi)闊視野的好方法。

















 
 
 












 
 
 
 