koa源碼中promise的解讀
koa 是一個(gè)非常輕量?jī)?yōu)雅的 node 應(yīng)用開(kāi)發(fā)框架,趁著雙十一值班的空當(dāng)閱讀了下其源代碼,其中一些比較有意思的地方整理成文與大家分享一下。
洋蔥型中間件機(jī)制的實(shí)現(xiàn)原理
我們經(jīng)常把 koa 中間件的執(zhí)行機(jī)制類比于剝洋蔥,這樣設(shè)計(jì)其執(zhí)行順序的好處是我們不再需要手動(dòng)去管理 request 和 response 的業(yè)務(wù)執(zhí)行流程,且一個(gè)中間件對(duì)于 request 和 response 的不同邏輯能夠放在同一個(gè)函數(shù)中,可以幫助我們極大的簡(jiǎn)化代碼。在了解其實(shí)現(xiàn)原理之前,先來(lái)介紹一下 koa 的整體代碼結(jié)構(gòu):
lib |-- application.js |-- context.js |-- request.js |-- response.js
application 是整個(gè)應(yīng)用的入口,提供 koa constructor 以及實(shí)例方法屬性的定義。context 封裝了koa ctx 對(duì)象的原型對(duì)象,同時(shí)提供了對(duì) response 和 request 對(duì)象下許多屬性方法的代理訪問(wèn),request.js 和 response.js 分別定義了ctx request 和 response 屬性的原型對(duì)象。
接下來(lái)讓我們來(lái)看 application.js中的一段代碼:
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
上述代碼展示了 koa 的基本原理,在其實(shí)例方法 listen 中對(duì) http.createServer 進(jìn)行了封裝 ,然后在回調(diào)函數(shù)中執(zhí)行 koa 的中間件,在 callback 中,this.middleware 為業(yè)務(wù)定義的中間件函數(shù)所構(gòu)成的數(shù)組,compose 為 koa-compose 模塊提供的方法,它對(duì)中間件進(jìn)行了整合,是構(gòu)建 koa 洋蔥型中間件模型的奧妙所在。從 handleRequest 方法中可以看出 compose 方法執(zhí)行返回的是一個(gè)函數(shù),且該函數(shù)的執(zhí)行結(jié)果是一個(gè) promise。接下來(lái)我們就來(lái)一探究竟,看看 koa-compose 是如何做到這些的,其 源代碼和一段 koa 中間件應(yīng)用示例代碼如下所示:
// compose源碼
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
/*
** 中間件應(yīng)用示例代碼
*/
let Koa = require('koa')
let app = new Koa()
app.use(async function ware0 (ctx, next) {
await setTimeout(function () {
console.log('ware0 request')
}, 0)
next()
console.log('ware0 response')
})
app.use(function ware1 (ctx, next) {
console.log('ware1 request')
next()
console.log('ware1 response')
})
// 執(zhí)行結(jié)果
ware0 request
ware1 request
ware1 response
ware0 response
從上述 compose 的源碼可以看出,每個(gè)中間件所接受的 next 函數(shù)入?yún)⒍际窃?compose 返回函數(shù)中定義的 dispatch 函數(shù),dispatch接受下一個(gè)中間件在 middlewares 數(shù)組中的索引作為入?yún)?,該索引就像一個(gè)游標(biāo)一樣,每當(dāng) next 函數(shù)執(zhí)行后,游標(biāo)向后移一位,以獲取 middlaware 數(shù)組中的下一個(gè)中間件函數(shù) 進(jìn)行執(zhí)行,直到數(shù)組中最后一個(gè)中間件也就是使用 app.use 方法添加的最后一個(gè)中間件執(zhí)行完畢之后再依次 回溯執(zhí)行。整個(gè)流程實(shí)際上就是函數(shù)的調(diào)用棧,next 函數(shù)的執(zhí)行就是下一個(gè)中間件的執(zhí)行,只是 koa 在函數(shù)基礎(chǔ)上加了一層 promise 封裝以便在中間件執(zhí)行過(guò)程中能夠?qū)⒉东@到的異常進(jìn)行統(tǒng)一處理。 以上述編寫的應(yīng)用示例代碼作為例子畫出函數(shù)執(zhí)行調(diào)用棧示意圖如下:

整個(gè) compose 方法的實(shí)現(xiàn)非常簡(jiǎn)潔,核心代碼僅僅 17 行而已,還是非常值得圍觀學(xué)習(xí)的。
generator函數(shù)類型中間件的執(zhí)行
v1 版本的 koa 其中間件主流支持的是 generator 函數(shù),在 v2 之后改而支持 async/await 模式,如果依舊使用 generator,koa 會(huì)給出一個(gè) deprecated 提示,但是為了向后兼容,目前 generator 函數(shù)類型的中間件依然能夠執(zhí)行,koa 內(nèi)部利用 koa-convert 模塊對(duì) generator 函數(shù)進(jìn)行了一層包裝,請(qǐng)看代碼:
function convert (mw) {
// mw為generator中間件
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
if (mw.constructor.name !== 'GeneratorFunction') {
// assume it's Promise-based middleware
return mw
}
const converted = function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}
function * createGenerator (next) {
return yield next()
}
從上面代碼可以看出,koa-convert 在 generator 外部包裹了一個(gè)函數(shù)來(lái)提供與其他中間件一致的接口,內(nèi)部利用 co 模塊來(lái)執(zhí)行 generator 函數(shù),這里我想聊的就是 co 模塊的原理,generator 函數(shù)執(zhí)行時(shí)并不會(huì)立即執(zhí)行其內(nèi)部邏輯,而是返回一個(gè)遍歷器對(duì)象,然后通過(guò)調(diào)用該遍歷器對(duì)象的 next 方法來(lái)執(zhí)行,generator 函數(shù)本質(zhì)來(lái)說(shuō)是一個(gè)狀態(tài)機(jī),如果內(nèi)部有多個(gè) yield 表達(dá)式,就需要 next 方法執(zhí)行多次才能完成函數(shù)體的執(zhí)行,而 co 模塊的能力就是實(shí)現(xiàn) generator 函數(shù)的 自動(dòng)執(zhí)行,不需要手動(dòng)多次調(diào)用 next 方法,那么它是如何做到的呢?co 源碼如下:
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
if (typeof gen === "function") gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== "function") return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
// toPromise是一個(gè)函數(shù),返回一個(gè)promise示例
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
"You may only yield a function, promise, generator, array, or object, " +
'but the following object was passed: "' +
String(ret.value) +
'"'
)
);
}
});
}
從 co 源碼來(lái)看,它先是手動(dòng)執(zhí)行了一次onFulfilled 函數(shù)來(lái)觸發(fā) generator 遍歷器對(duì)象的 next 方法,然后利用promise的onFulfilled 函數(shù)去自動(dòng)完成剩余狀態(tài)機(jī)的執(zhí)行,在onRejected 中利用遍歷器對(duì)象的 throw 方法拋出執(zhí)行上一次 yield 過(guò)程中遇到的異常,整個(gè)實(shí)現(xiàn)過(guò)程可以說(shuō)是相當(dāng)簡(jiǎn)潔優(yōu)雅。
結(jié)語(yǔ)
通過(guò)上面的例子可以看出 promise 的能量是非常強(qiáng)大的,koa 的中間件實(shí)現(xiàn)和 co 模塊的實(shí)現(xiàn)都是基于 promise,除了應(yīng)用于日常的異步流程控制,在開(kāi)發(fā)過(guò)程中我們還可以大大挖掘其潛力,幫助我們完成一些自動(dòng)化程序工作流的事情。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于沒(méi)有徹底清除node和npm的解決方法
最近因?yàn)閚pm和node的版本不匹配遇到了許多困難,比如更新npm的版本,結(jié)果它給報(bào)錯(cuò)了,查閱了好多資料,發(fā)現(xiàn)就是node和npm卸載的時(shí)候,還有殘留,所以本文給大家介紹了關(guān)于沒(méi)有徹底清除node和npm的解決方法,需要的朋友可以參考下2024-03-03
Node.js內(nèi)置模塊events事件監(jiān)聽(tīng)發(fā)射詳解
這篇文章主要為大家介紹了Node.js內(nèi)置模塊events事件監(jiān)聽(tīng)發(fā)射詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
nodejs實(shí)現(xiàn)大文件(在線視頻)的讀取
這篇文章主要為大家詳細(xì)介紹了nodejs實(shí)現(xiàn)大文件的讀取,比如在線視頻,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12
node.js?express和koa中間件機(jī)制和錯(cuò)誤處理機(jī)制
這篇文章主要介紹了node.js?express和koa中間件機(jī)制和錯(cuò)誤處理機(jī)制,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07
從一個(gè)爬蟲(chóng)開(kāi)始ChatGPT的編程秀
這篇文章主要為大家介紹了從一個(gè)爬蟲(chóng)開(kāi)始ChatGPT的編程秀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
使用Node.js自動(dòng)生成帶動(dòng)態(tài)圖表的Word文檔
在現(xiàn)代軟件開(kāi)發(fā)中,動(dòng)態(tài)生成?Word?文檔是一項(xiàng)非常常見(jiàn)的需求,本文將結(jié)合Node.js和ECharts實(shí)現(xiàn)自動(dòng)生成帶動(dòng)態(tài)圖表的Word文檔,感興趣的可以了解下2024-03-03
優(yōu)化Node.js Web應(yīng)用運(yùn)行速度的10個(gè)技巧
這篇文章主要介紹了優(yōu)化Node.js Web應(yīng)用運(yùn)行速度的10個(gè)技巧,本文講解了從并行、異步、緩存、gzip 壓縮、客戶端渲染等等技巧,需要的朋友可以參考下2014-09-09
使用puppeteer破解極驗(yàn)的滑動(dòng)驗(yàn)證碼
這篇文章主要介紹了利用puppeteer破解極驗(yàn)的滑動(dòng)驗(yàn)證功能,基本流程代碼實(shí)現(xiàn)給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-02-02

