大家好,我是若川。這是 源碼共讀活動《1個月,200+人,一起讀了4周源碼》 第四期,
紀年小姐姐
的第四次投稿。紀年小姐姐
通過本次學習提早接觸到generator
,協程概念,了解了async/await
函數的原理等。
第四期是 學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理中的
co原理
。不知不覺,源碼共讀已經進行了一個月,有些小伙伴表示對面試和工作很有幫助,學完立馬能用。如果你也感興趣可以加我微信ruochuan12
參加。
1. 前言
這周看的是 co 的源碼,我對 co 比較陌生,沒有了解和使用過。因此在看源碼之前,我希望能大概了解 co 是什么,解決了什么問題。
2. 簡單了解 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.
看起來有點懵逼,又查了一些資料,大多說 co 是用于 generator 函數的自動執行。generator 是 ES6 提供的一種異步編程解決方案,它最大的特點是可以控制函數的執行。
2.1 關于 generator
說到異步編程,我們很容易想到還有 promise,async 和 await。它們有什么區別呢?先看看 JS 異步編程進化史:callback -> promise -> generator -> async + await
再看看它們語法上的差異:
Callback | Promise | Generator | async + await + Promise |
---|---|---|---|
ajax(url, () => {}) | Promise((resolve,reject) => { resolve() }).then() | function* gen() { yield 1} | async getData() { ?await fetchData() } |
關于 generator 的學習不在此篇幅詳寫了,需要了解它的概念和語法。
3. 學習目標
經過簡單學習,大概明白了 co 產生的背景,因為 generator 函數不會自動執行,需要手動調用它的 next() 函數,co 的作用就是自動執行 generator 的 next() 函數,直到 done 的狀態變成 true 為止。
那么我這一期的學習目標:
1)解讀 co 源碼,理解它是如何實現自動執行 generator
2)動手實現一個簡略版的 co
4. 解讀 co 源碼
co 源碼地址:https://github.com/tj/co
4.1 整體架構
從 README 中,可以看到是如何使用 co :
co(function*?()?{var?result?=?yield?Promise.resolve(true);return?result;
}).then(function?(value)?{console.log(value);
},?function?(err)?{console.error(err.stack);
});
從代碼可以看到它接收了一個 generator 函數,返回了一個 Promise,這部分對應的源碼如下。
function?co(gen)?{var?ctx?=?this;//?獲取參數var?args?=?slice.call(arguments,?1);//?返回一個?Promisereturn?new?Promise(function(resolve,?reject)?{//?把?ctx?和參數傳遞給?gen?函數if?(typeof?gen?===?'function')?gen?=?gen.apply(ctx,?args);//?判斷?gen.next?是否函數,如果不是直接?resolve(gen)if?(!gen?||?typeof?gen.next?!==?'function')?return?resolve(gen);//?先執行一次?nextonFulfilled();//?實際上就是執行?gen.next?函數,獲取?gen?的值function?onFulfilled(res)?{var?ret;try?{ret?=?gen.next(res);}?catch?(e)?{return?reject(e);}next(ret);return?null;}//?對?gen.throw?的處理function?onRejected(err)?{var?ret;try?{ret?=?gen.throw(err);}?catch?(e)?{return?reject(e);}next(ret);}//?實際處理的函數,會遞歸執行,直到?ret.done?狀態為?truefunction?next(ret)?{//?如果生成器的狀態?done?為?true,就?resolve(ret.value),返回結果if?(ret.done)?return?resolve(ret.value);//?否則,將?gen?的結果?value?封裝成?Promisevar?value?=?toPromise.call(ctx,?ret.value);//?判斷?value?是否?Promise,如果是就返回?thenif?(value?&&?isPromise(value))?return?value.then(onFulfilled,?onRejected);//?如果不是?Promise,Rejectedreturn?onRejected(new?TypeError('You?may?only?yield?a?function,?promise,?generator,?array,?or?object,?'+?'but?the?following?object?was?passed:?"'?+?String(ret.value)?+?'"'));}});
}
看到這里,我產生了一個疑問:Promise + then 也可以處理異步編程,為什么 co 的源碼里要把 Promise + generator 結合起來呢,為什么要這樣做?直到我搞懂了 co 的核心目的,它使 generator 和 yield 的語法更趨向于同步編程的寫法,引用阮一峰的網絡日志中的一句話就是:
異步編程的語法目標,就是怎樣讓它更像同步編程。
可以看一個 Promise + then 的例子:
function?getData()?{return?new?Promise(function(resolve,?reject)?{resolve(1111)})
}
getData().then(function(res)?{//?處理第一個異步的結果console.log(res);//?返回第二個異步return?Promise.resolve(2222)
})
.then(function(res)?{//?處理第二個異步的結果console.log(res)
})
.catch(function(err)?{console.error(err);
})
如果有多個異步處理就會需要寫多少個 then 來處理異步之間可能存在的同步關系,從以上的代碼可以看到 then 的處理是一層一層的嵌套。如果換成 co,在寫法上更優雅也更符合日常同步編程的寫法:
co(function*?()?{try?{var?result1?=?yield?Promise.resolve(1111)//?處理第一個異步的結果console.log(result1);//?返回第二個異步var?result2?=?yield?Promise.resolve(2222)//?處理第二個異步的結果console.log(result2)}?catch?(err)?{console.error(err)}
});
4.2 分析 next 函數
源碼的 next 函數接收一個 gen.next() 返回的對象 ret 作為參數,形如{value: T, done: boolean}
,next 函數只有四行代碼。
第一行:if (ret.done) return resolve(ret.value);
如果 ret.done 為 true,表明 gen 函數到了結束狀態,就 resolve(ret.value),返回結果。
第二行:var value = toPromise.call(ctx, ret.value);
調用 toPromise.call(ctx, ret.value) 函數,toPromise 函數的作用是把 ret.value 轉化成 Promise 類型,也就是用 Promise 包裹一層再 return 出去。
function?toPromise(obj)?{//?如果?obj?不存在,直接返回?objif?(!obj)?return?obj;//?如果?obj?是?Promise?類型,直接返回?objif?(isPromise(obj))?return?obj;//?如果?obj?是生成器函數或遍歷器對象,?就遞歸調用?co?函數if?(isGeneratorFunction(obj)?||?isGenerator(obj))?return?co.call(this,?obj);//?如果?obj?是普通的函數類型,轉換成?Promise?類型函數再返回if?('function'?==?typeof?obj)?return?thunkToPromise.call(this,?obj);//?如果?obj?是一個數組,?轉換成?Promise?數組再返回if?(Array.isArray(obj))?return?arrayToPromise.call(this,?obj);//?如果?obj?是一個對象,?轉換成?Promise?對象再返回if?(isObject(obj))?return?objectToPromise.call(this,?obj);//?其他情況直接返回return?obj;
}
第三行:if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
如果 value 是 Promise 類型,調用 onFulfilled 或 onRejected,實際上是遞歸調用了 next 函數本身,直到 done 狀態為 true 或 throw error。
第四行:return onRejected(...)
如果不是 Promise,直接 Rejected。
5. 實踐
雖然解讀了 co 的核心代碼,看起來像是懂了,實際上很容易遺忘。為了加深理解,結合上面的 co 源碼和自己的思路動手實現一個簡略版的 co。
5.1 模擬請求
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 模擬實現簡版 co
核心實現:
1)函數傳參
2)generator.next 自動執行
function?co(gen)?{//?1.?傳參var?ctx?=?this;const?args?=?Array.prototype.slice.call(arguments,?1);gen?=?gen.apply(ctx,?args);return?new?Promise(function(resolve,?reject)?{//?2.?自動執行?nextonFulfilled()function?onFulfilled?(res)?{var?ret?=?gen.next(res);next(ret);}function?next(ret){if?(ret.done)?return?resolve(ret.value);//?此處只處理?ret.value?是?Promise?對象的情況,其他類型簡略版沒處理var?promise?=?ret.value;//?自動執行promise?&&?promise.then(onFulfilled);}})
}//?執行
co(function*?getData()?{var?result?=?yield?request();//?1s后打印?{data:?"request"}console.log(result)
})
6. 感想
對我來說,學習一個新的東西(generator)花費的時間遠遠大于單純閱讀源碼的時間,因為需要了解它產生的背景,語法,解決的問題以及一些應用場景,這樣在閱讀源碼的時候才知道它為什么要這樣寫。
讀完源碼,我們會發現,其實 co 就是一個自動執行 next() 的函數,而且到最后我們會發現 co 的寫法和我們日常使用的 async/await 的寫法非常相像,因此也不難理解【async/await 實際上是對 generator 封裝的一個語法糖】這句話了。
//?co?寫法
co(function*?getData()?{var?result?=?yield?request();//?1s后打印?{data:?"request"}console.log(result)
})
//?async?await?寫法
(async?function?getData()?{var?result?=?await?request();//?1s后打印?{data:?"request"}console.log(result)
})()
不得不說,閱讀源碼的確是一個開闊視野的好方法,如果不是這次活動,我可能還要晚個大半年才接觸到 generator,接觸協程的概念,了解到 async/await 實現的原理,希望能夠繼續堅持下去~
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我讀源碼的經歷
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,活躍在知乎@若川,掘金@若川。致力于分享前端開發經驗,愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~