Koa 生成器函式探尋
雖然 Koa 要在下一個 major 版本里移除對生成器 generator 的支援, 但是看一看它對生成器的處理還是能夠加深我們對生成器的理解的.
Koa 原始碼中和生成器有關的程式碼就以下幾行, 判斷use
方法新增的函式是否是生成器函式, 是的話, 將它轉換成非同步函式. 其中呼叫的兩個函式都是由周邊庫提供的.
if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } 複製程式碼
isGeneratorFunction
這些依賴都是很短小的單檔案, 不如全部貼上過來.
判斷函式是否是一個生成器函式.
'use strict'; var toStr = Object.prototype.toString; var fnToStr = Function.prototype.toString; var isFnRegex = /^\s*(?:function)?\*/; // 這個似乎是用來檢測當前執行環境有沒有引入生成器函式, 還要再看看 var hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; var getProto = Object.getPrototypeOf; var getGeneratorFunc = function () { // eslint-disable-line consistent-return // 如果沒有 hasToStringTag, 直接返回 false 表示無法生成生成器函式 if (!hasToStringTag) { return false; } // 否則嘗試利用 Function 生成一個生成器函式並返回 try { return Function('return function*() {}')(); } catch (e) { } }; var generatorFunc = getGeneratorFunc(); // 如果沒有返回生成器函式, 返回一個空物件, 這樣最後的判定就會失敗 // 如果返回了一個生成器函式, 得到生成器函式的原型物件 var GeneratorFunction = generatorFunc ? getProto(generatorFunc) : {}; module.exports = function isGeneratorFunction(fn) { // 不是函式的話肯定也不是生成器函式 if (typeof fn !== 'function') { return false; } // 將這個函式轉換成 string, 然後檢視函式字面中是否包含 function*, 有則是一個生成器函式 // 但是這個判斷是很不嚴謹的, 因為它強制要求寫法為 function* // 而 function *boo 就沒有辦法識別了 if (isFnRegex.test(fnToStr.call(fn))) { return true; } // 如果上面的方法不行, 就嘗試利用 toString 的方法 if (!hasToStringTag) { var str = toStr.call(fn); return str === '[object GeneratorFunction]'; } // 最後的方法, 通過原型物件判別 return getProto(fn) === GeneratorFunction; }; 複製程式碼
convert
並不是將生成器函式轉換成非同步函式, 而是讓它能融入到 Koa 2.0 的工作流程中.
'use strict' const co = require('co') const compose = require('koa-compose') module.exports = convert function convert (mw) { 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 } // 真正核心的程式碼就這三行 // 返回了一個符合 koa 中介軟體函式簽名要求的函式, 這個函式內部呼叫了 co const converted = function (ctx, next) { // co 函式和中介軟體在執行的時候, 繫結上下文到 ctx, 也就是 koa 的 context // mw.call 的時候, 返回了一個迭代器, 然後 co 去執行這個迭代器, 最終返回一個 Promise // 到這裡我們有必要知道 koa 要求如何寫一個生成器 return co.call(ctx, mw.call(ctx, createGenerator(next))) } converted._name = mw._name || mw.name return converted } // 這裡的生成器返回了迭代器, 當在使用者的生成器函式中呼叫 function * createGenerator (next) { return yield next() } // 後面兩個方法沒有用到, 省略 複製程式碼
Koa 對生成器的寫法要求
在 Koa 1.x 版本中, 中介軟體要求是生成器函式, 寫法如下:
function * legacyMiddleWare(next) { yield next } 複製程式碼
可以看到,createGenerator(next)
返回的迭代器就是這裡的 next.
co
co 是一個迭代器的執行器, 返回一個 Promise.
/** * slice() reference. */ var slice = Array.prototype.slice; /** * Execute the generator function or a generator * and return a Promise. * * @param {Function} fn * @return {Promise} * @api public */ 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); // 如果不存在迭代器或者迭代器沒有 next, 那麼直接返回一個 resolved 狀態的 Promise if (!gen || typeof gen.next !== 'function') return resolve(gen); // 執行這個迭代器 onFulfilled(); /** * 對迭代器進行一次 next 呼叫 * * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { // 利用上次得到的結果對迭代器進行 next 呼叫, 得到 yield 出的返回值 ret = gen.next(res); } catch (e) { // 有錯直接返回出一個拒絕態的 Promise return reject(e); } // 如果沒出錯就通過 next 進行處理 next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { // 如果出錯的話會呼叫迭代器的 throw 嘗試解決錯誤 ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a Promise. * * 得到迭代器的下一個值, 並返回一個 Promise * * @param {Object} ret * @return {Promise} * @api private */ function next(ret) { // 如果迭代已執行完成, 返回一個 resolved 狀態的 Promise, resolved undefined if (ret.done) return resolve(ret.value); // 否則將 value 包裝成一個 Promise var value = toPromise.call(ctx, ret.value); // 如果是包裝了 truthy 值的 Promise, 那麼通過 then 來後處理 // 這裡的 value 實際是 createGenerator 返回的迭代器封裝好的 Promise if (value && isPromise(value)) return value.then(onFulfilled, onRejected); // 如果不能封裝為 Promise 則丟擲錯誤 return onRejected(new TypeError('You may only yield a function, Promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); } /** * Convert a `yield`ed value into a Promise. * * 針對 yield 的 value 可能具有的不同情形來封裝成 Promise * * @param {Mixed} obj * @return {Promise} * @api private */ function toPromise(obj) { // 如果為 falsy 直接返回 if (!obj) return obj; // 如果是 Promise 直接返回 if (isPromise(obj)) return obj; // 如果是迭代器或者是生成器就用 co 再執行 // 實際上 koa 走的是這個分支, 它會再用 co 執行這個迭代器, 返回 Promise // 迭代器在執行的時候, 就往 koa middleware 的下游走 if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); // 如果是一個 function 那麼就通過 thunkToPromise 封裝, 不展開了 if ('function' == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; } /** * Convert a thunk to a Promise. * * 其他的輔助方法從略, 只看看這個. * * @param {Function} * @return {Promise} * @api private */ function thunkToPromise(fn) { var ctx = this; return new Promise(function (resolve, reject) { fn.call(ctx, function (err, res) { if (err) return reject(err); if (arguments.length > 2) res = slice.call(arguments, 1); resolve(res); }); }); } 複製程式碼
convert
的執行過程
-
當 Koa 要執行生成器函式轉換成的中介軟體的時候, 即呼叫
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
時, 執行return co.call(ctx, mw.call(ctx, createGenerator(next)))
, 它返回一個 Promise -
其中, 使用者提供的生成器
mw
被呼叫, 同時呼叫createGenerator(next)
返回一個迭代器 -
co 呼叫自己的
onFulfilled
方法執行使用者的迭代器. 使用者會寫yield next
這一句, 將控制權交還給 co, co 呼叫next
方法. 此時, 由於ret.value
是createGenerator(next)
返回的迭代器, 所以next
方法進入if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
的分支 -
value
被封裝成一個 Promise, 其實內部又用了一次 co 對return yield next
進行執行 -
return yield next
被執行, 進入下游 middleware 並最終回溯到當前的 middleware -
co 第二次執行
onFulfilled
, 然後呼叫next
方法, 此時ret.done
為真, 返回一個解決態的 Promise -
這裡就回到了
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
, 繼續往上游回溯
到這裡, 我們就梳理清楚了 Koa 1.x 時代所採用的生成器函式是如何被 Koa 2.x 所採用的非同步函式相容的.
可能需要畫張圖來更清楚地展示這個過程.