0.前言
在上一篇文章裡,已經對v2版本的koa中介軟體原理做了逐行分析,講清楚了它的流程控制和非同步方案。
但是,仍然有大量的基於koa的專案、框架、庫在基於v1版本的koa在工作,而它的中介軟體是Generator函式,其執行機制與v2版本的koa中介軟體有比較大的不同。
因此,有必要解釋清楚v1版本的koa中介軟體原理,作為對上一篇文章的補充,希望能對那些仍然在專案中使用v1版本koa的同行同學有所幫助。
PS:本文基於v1.6.2的koa原始碼,結構圖如下:
1.響應機制
本段內容與上一篇文章大致相同,已經看過該文章的話,可以跳過這一節
與上一篇文章一樣,先從一個簡單的Demo開始,來看Koa的使用方式。
const Koa = require('koa');
const app = new Koa();
複製程式碼
Koa變數指向是什麼呢?我們知道:
require在查詢第三方模組時,會查詢該模組下package.json檔案的main欄位。
檢視koa倉庫目錄下下package.json檔案,可以看到模組暴露的出口是lib目錄下的application.js檔案
{
"main": "lib/application.js",
}
複製程式碼
在lib/application
檔案中,可以看到其模組出口如下:
var app = Application.prototype;
module.exports = Application;
function Application(){}
複製程式碼
好,現在來給我們的Demo新增中介軟體
const Koa = require('koa');
const app = new Koa();
const one = function* (next){
console.log('1-Start');
const t = yield next;
console.log('1-End');
}
const final = function* (next) {
console.log('final-Start');
this.body = { text: 'Hello World' };
console.log('final-End');
}
app.use(one);
app.use(final);
app.listen(3005);
複製程式碼
以上這段程式碼中,ctx.body 如何實現並不是本文的重點,只要知道它的作用是設定響應體的資料,就可以了。
但是要弄清楚的關鍵有亮點
- app.use 的作用是掛載中介軟體,它做了什麼?
- app.listen 的作用是監聽埠,它做了哪些工作?
先來看use函式
app.use = function(fn){
// 省略與中介軟體機制無關的程式碼
this.middleware.push(fn);
return this;
};
複製程式碼
根據上文提到的var app = Application.prototype;
語句,可以知道use方法掛載到了Application.prototype上,因此每個例項都可以呼叫use。
use方法做的事情也很簡單,把傳入的函式,存入到例項的middleware陣列當中。
再來看listen方法:
app.listen = function(){
// 省略無關程式碼
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
複製程式碼
如果曾使用過Node的[http]模組建立過簡單的伺服器應用的話,就會知道http.createServer的引數一個函式,函式的引數分別是請求物件request和相應物件response,即形如以下結構:
(req, res) => {
// Do Sth.
res.end('Hello World')
}
複製程式碼
因此this.callback函式執行所返回的結果,也一定是這樣一個結構,我們來看這個callback函式
app.callback = function(){
// 省略一些校驗程式碼
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
var self = this;
// 省略一些錯誤處理程式碼,與中介軟體機制沒關係
return function handleRequest(req, res){
var ctx = self.createContext(req, res);
self.handleRequest(ctx, fn);
}
};
複製程式碼
看到返回的handleRequest函式的結構了嗎?它會被傳遞給http.createServer,因此在每次接收到請求時,都會執行該函式。
那好,再來看self.handleRequest函式。
app.handleRequest = function(ctx, fnMiddleware){
ctx.res.statusCode = 404;
onFinished(ctx.res, ctx.onerror);
fnMiddleware.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
};
複製程式碼
這個函式接受一個上下文ctx物件作為第一個引數,這裡只要記住它是一個物件就可以,後面會被傳遞給每個中介軟體。它的第二個引數,是一個名為fnMiddleware的函式,它是什麼呢?回過頭去看app.handleRequest
被執行的地方。
self.handleRequest(ctx, fn);
複製程式碼
這裡的fn又是什麼?再去找fn的定義,發現
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
複製程式碼
原來fn是this.middleware經過一些處理後得到的函式,這些工作具體做了什麼,後文會說。這裡只要先記住fn是一個組合後的函式,執行了它,那麼一系列中介軟體就會依次執行。
現在fn清楚了,也就是self.handleRequest
函式的第二個引數就清楚了,接著剛剛沒有說完的話題,看看這個self.handleRequest
做了什麼事。
function(ctx, fnMiddleware){
// 忽略其他非關鍵程式碼
fnMiddleware.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
};
複製程式碼
也很簡單,以上下文ctx物件作為this,去呼叫fnMiddleware函式其返回的結果是一個Promise,並且使用了該Promise的then/catch方法,新增了兩個流程:
- 請求相應:
respond.call(ctx)
- 錯誤處理:
.catch(ctx.onerror)
所以,總結一下:
- use方法將中介軟體函式新增到自身的middleware陣列上
- listen方法設定響應請求的函式,該函式中會執行中介軟體。當每次請求發起時,所執行的流程就是:請求 -> handleRequest函式 -> self.handleRequest函式 -> fnMiddleware函式。
好,v1版本的響應機制已經介紹完畢,各位也差不多能知道中介軟體是在什麼時候執行的了。如果有不明白的地方,可以先看完上一篇講v2版本Koa原理的文章(傳送門)。
2.前置知識-Generator
與之前基於v2版本的koa相比,v1版本的koa,在中介軟體原理上有個重大的不同之處,即
v1版本的koa的執行工作,是交給Co庫完成的,而v2則是自己完成的。
這話什麼意思???這與Koa使用Generator函式作為中介軟體函式有關。
所以,在正式開始介紹v1版本koa的中介軟體原理之前,有一些前置知識要先解釋清楚。
(已經熟悉Generator和Co庫原理的各位,可以直接跳過介紹這兩章)
2.1 Generator
Generator 是一種特殊的函式,它的執行結果是一個Iterator,即可迭代物件。
那Iterator又是什麼呢? 可以這麼說,任何一個物件,只要符合迭代器協議,就可以被認為是一個Iterator。通俗一點說,該協議所約定的物件,有兩個關鍵點:
- 有next方法,連續執行next方法可以依次得到多個值
- 執行next方法,返回值的格式為 { value: xxx, done: Boolean },value為任意型別,done的值為Boolean,true表示迭代完成,false表示迭代未完成。
示例如下,下面的it物件,因為符合迭代器協議,就是一個Iterator(儘管它不是由Generator返回的)
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
複製程式碼
那麼Iterator有什麼用呢?它原本的作用,是用於自定義物件的迭代行為。
而為什麼又要定義物件的迭代行為呢?按照阮一峰老師在Iterator和for...of迴圈裡的說法
遍歷器(Iterator)就是這樣一種機制。它是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。
對此我的理解是,“定義物件的迭代行為”的意義,在於“為各種不同的資料結構提供統一的訪問機制”,即把對於迭代的描述工作交給了資料結構自身,開發者只需呼叫單個API即可(即for...of...),這將會節約開發者記憶API的成本。正所謂“物件千萬種,規範第一條;迭代不規範,開發淚兩行”...
而如果要定義物件的迭代行為,就要在它的[Symbol.iterator]屬性上,定義一個函式,並且返回一個符合迭代器協議的物件(即Iterator)。(參考:可迭代協議)。
比如,我們把上面的程式碼改一改,就可以使用for...of...來執行迭代了。
const arr = ['a', 'b'];
arr[Symbol.iterator] = function () {
let i = 0;
return {
next: function () {
const done = i >= arr.length;
if (!done) {
console.log('Not Done!')
return { value: arr[i++], done };
} else {
return { done };
}
}
}
}
for(let i of arr){}; // 輸出兩次Not Done
複製程式碼
但是從上面的程式碼可以看到,何時完成迭代(控制done的值),每次迭代返回value值是什麼,都交給了next函式來維護,需要寫的維護程式碼較多。
這時候我們可以回到Generator函式了,因為Generator函式就是為此而生的。它為這種維護工作,提供了簡化的寫法,只要通過yield命令給出返回值就可以了,最後yield全部結束的時候。
arr[Symbol.iterator] = function* () {
for(let i = 0; i < arr.length; i++){
console.log(`輸出文字:yield arr[${i}]`)
yield arr[i]
}
return ;
}
for(let i of arr){};
// 輸出文字:yield arr[0]
// 輸出文字:yield arr[1]
複製程式碼
所以,個人認為,Generator函式是一種語法糖,它描述的是若干個程式碼塊的分段執行順序。
2.2 Generator與非同步
既然Generator有分段執行的功能,就可以處理非同步問題了。
不過,值得注意的是,yield並不是真正的非同步邏輯,它只是把yield後面的值,在執行next方法的時候返回出去而已。比如當yield 後面的表示式返回的是一個Promise的時候:
function* gen1(){
yield 1;
yield new Promise(resolve => setTimeout(() => resolve(1), 1000));
yield 2;
}
const iterator = gen1();
複製程式碼
開始跑iterator的next方法
iterator.next(); // {value: 1, done: false};
iterator.next(); // {value: Promise, done: false};
iterator.next(); // {value: 2, done: false};
複製程式碼
看到了嗎?第二句next方法只是返回了Promise物件而已,根本沒有等著它的then回撥執行。
所以,要想用Generator來做非同步操作,其基本思路只能是如下:執行第一次next -> 等待promise 完成 -> 執行第二次next...舉個例子:
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
// value是個Promise,下一次g.next執行要交給promise的then回撥
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
複製程式碼
但是,這顯然有問題:gen
函式返回的Iterator,必須手動執行才能進行到下一個yield,不會自動執行。如果非同步方案僅僅是如此,那開發者還不如自己寫Promise鏈呢。
這就是co的作用了!它會:
- 把yield 變成“真正的非同步等待語句”
- 自動執行next
3.前置知識-Co
3.1 基本使用方式
通常來說,我們使用co庫,會像是現在這樣
const co = require('co');
const mockTimeoutPromise = (data, timeout = 1000) => new Promise(resolve => {
setTimeout(() => {
console.log(data);
resolve(data)
}, timeout);
});
co(function* () {
yield mockTimeoutPromise(1);
const result = yield { name: 'co' };
return result;
})
.then(value => {
console.log(value);
})
複製程式碼
接下來我們就用這份原始碼,來分析co庫到底是怎麼做的
3.2 執行分析
可以看到,co函式接受了一個Generator函式作為引數。
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
});
}
複製程式碼
可以看到,co函式的返回值是一個Promise,它對應的是當次co函式內包裹的整個流程,一旦該Promise被resolved,就意味著co函式所接受的入參函式,其內部流程已經完全執行完畢。
好,接下來來Promise建構函式內部的內容。
首先是一段執行傳入的Generator函式的程式碼,獲得Iterator
// 在某些情況下,傳入的gen引數本身就是一個Iterator物件,不是Generator函式,因此會做型別檢查
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 確保獲得的結果,是一個Iterator
if (!gen || typeof gen.next !== 'function') return resolve(gen);
複製程式碼
接著就是開始執行onFulfilled函式。
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
複製程式碼
在初次執行時,入參res的值為空,以剛剛提供的Demo來看,執行gen.next(res)得到的ret值,應該是這樣的結構:
{
done: false, value: Promise
}
複製程式碼
也就是說,其value值,是Demo程式碼中mockTimeoutPromise
函式的執行結果。
接著,就把ret值傳入next函式
function next(ret) {
if (ret.done) return resolve(ret.value);
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) + '"'));
}
複製程式碼
分析這段程式碼,其執行邏輯為:
- 檢查傳入的Iterator所對應的迭代流程是否已經結束,若結束,說明resolve函式結束整個流程。
- 用
toPromise
函式將當前的迭代值,變成Promise型別 - 判斷處理後的值是否存在 且 為Promise,則新增一個then回撥,繼續執行onFulfilled和用onRejected處理錯誤。
- 若co所執行的流程內,yield關鍵字後跟著的不是yieldable型別的,則丟擲錯誤。
在初次流程中,由於是第一句yield,所以ret.done為false,開始執行toPromise邏輯,接著來看toPromise函式
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
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;
}
複製程式碼
這段程式碼的主要功能是將各種型別的值,包裹成了Promise值,其中各個轉換函式的邏輯在這裡不是重點,因此不加詳細闡述。
因此,如果我們寫出這樣的程式碼
yield 2;
yield 3;
複製程式碼
toPromise就會直接返回這些被yield的常量值,而不轉化為promise型別。這時候我們再回到toPromise被執行的位置,即next函式內部,就會發現無法通過value && isPromise(value)
校驗,就會走onRejected報錯。
這也是為什麼,我們有時候會看見這樣的錯誤
You may only yield a function, promise, generator, array, or object, but the following object was passed: "2"
就是因為我們yield了一個非yieldables的值。
回到toPromise函式,其中有兩個轉換邏輯非常值得一提:
一是對於Generator的識別:如果識別為是Generator函式或者Generator函式所返回的Iterator,就會再次用co包裹一層,返回一個Promise。
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
複製程式碼
其中:
isGeneratorFunction
函式識別引數是否為Generator函式isGenerator
函式識別引數是否被Generator函式返回的Iterator
私人吐槽時間:isGenerator函式也許改名為isIteratorOfGenerator也許更標準一點。
二則是objectToPromise
,通常我們在使用yield的時候,會看見這樣的邏輯:
const { result1, result2 } = yield {
result1: Promise1,
result2: Promise2
}
複製程式碼
這是怎麼做到的呢?其實是依靠objectToPromise
函式,它的程式碼邏輯如下:
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
複製程式碼
它主要是做了這麼幾件事:
- 基於同一個constructor構建一個空物件result,保證型別一致。
var results = new obj.constructor();
var keys = Object.keys(obj);
複製程式碼
- 新建一個promises陣列,標記當前物件中「有哪些key對應的值需要“等待”」
var promises = [];
複製程式碼
- 遍歷當前物件的key,把key物件的value執行toPromise函式
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
}
複製程式碼
- 如果成功轉換為Promise,執行defer函式,構建一個新Promise表示“等待原Promise完成後,再把獲得的結果值賦值到result物件上”,推到promises陣列當中,表明當前key對應的值需要“等待”之後,才可以獲得值。
if (promise && isPromise(promise)) defer(promise, key);
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
複製程式碼
- 如果是非yieldable值不能被轉成Promise型別,直接把該value值賦值到新的result物件上(這些值不需要等待就可以獲得結果,所以不需要把它們包裝之後推入到promises陣列中)
else results[key] = obj[key];
複製程式碼
- 等待所有被推入promises陣列中的Promise被resolve,即所有要等待的值,都等到了結果。
return Promise.all(promises).then(function () {
return results;
});
複製程式碼
好,回到剛剛的toPromise函式,由於Demo程式碼裡的第一句是
yield mockTimeoutPromise(1);
複製程式碼
所以toPromise函式會執行第二句
if (isPromise(obj)) return obj;
複製程式碼
因此可以通過next函式的isPromise校驗
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
複製程式碼
好,第一輪迴圈至此結束,再也沒有其他程式碼要執行了。
接下來,因為mockTimeoutPromise
函式預設的超時值timeout為1000,所以1秒之後,上面的value(是一個Promise)被resolve,開始繼續執行onFulfilled函式
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
複製程式碼
此時,ret的返回結構為
{
done: false,
value: { name: 'co' };
}
複製程式碼
因此會繼續執行 next函式 -> toPromise函式 -> objectToPromise函式。因此,toPromise函式會得到這樣一個結果
Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: Object
複製程式碼
因此也可以通過next函式的isPromise
校驗,至此第二輪yield結束。
接下來
ret = gen.next(res);
複製程式碼
得到的ret.done值為true,所以第三次執行next函式,就會走
if (ret.done) return resolve(ret.value);
複製程式碼
至此,整個流程結束。
3.3 小結
好,Co的工作流程已經大致理清楚,在此做個小結:
-> 包裹一層Promise,用以判斷當前整個非同步流程終結
-> 執行傳入的Generator函式,獲得Iterator
-> 執行Iterator物件的next方法,獲得當前值value/done
-> 若done為false,包裝value值為Promise,在該Promise的then/catch回撥中,執行下一次next
-> 若done為true,整個流程已終結,將外層Promise給resolved。
4.中介軟體執行
如果暫時沒有理解前置知識相關章節的程式碼,可以直接看下面的幾點總結(看懂的可以跳過)
Generator函式:
- 用於描述物件的迭代行為
- 返回的值,是一個具有next方法的物件,即所謂的Iterator物件
co函式庫
- 會幫助Generator函式進行自動執行next方法以進行迭代
- 把yield方法變成了真正的非同步邏輯。
好了,有了這些基礎知識,我們可以開始看中介軟體的執行了。
在第一節的“請求響應機制”部分提到,fnMiddlewawre是一個把所有中介軟體組合後得到的函式,它會在每次收到請求的時候執行,來看它的組合程式碼
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
複製程式碼
查遍koa v1.6.2的原始碼,發現experimental屬性並沒有賦值語句,所以我們可以認為,只要你不寫這樣的程式碼
const app = new Koa();
app.experimental = true;
複製程式碼
那麼,experimental始終會是undefined
,也就是一個falsy的值。它一定會走的是
co.wrap(compose(this.middleware));
複製程式碼
於是,我們就有兩件事情要說清楚:
- co.wrap做了什麼
- compose做了什麼
4.1 co-wrap
先說co-wrap,因為它非常簡單
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
複製程式碼
它接受一個函式fn
,返回另一個函式createPromise
。
從上面的程式碼可以知道,返回的createPromise
,就是fnMiddleware
函式,而fnMiddleware
的執行語句是這樣的:
fnMiddleware.call(ctx)
複製程式碼
所以createPromise
在得到執行時,內部的this,就是上下文ctx物件。
而這createPromise
函式的職責也很簡單,就是把傳入的fn引數,用co庫來執行了一遍。
所以,我們來看compose函式做了什麼。
4.2 koa-compose
查詢頂部引入模組的語句,可以看到
var compose = require('koa-compose');
複製程式碼
好,我們來看koa-compose的模組,到底是什麼功能
PS:koa-compose基於2.5.1;
module.exports = compose;
// 空函式
function *noop(){}
function compose(middleware){
// 這個被返回的函式,就是傳給co-wrap的fn引數
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
複製程式碼
我們還是用一個Demo來看:
const Koa = require('koa');
const app = new Koa();
const one = function* (next){
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield next;
console.log('1-End');
}
const final = function* (next) {
console.log('final-Start');
this.body = { text: 'Hello World' };
console.log('final-End');
}
app.use(one);
app.use(final);
app.listen(3005);
複製程式碼
因此,middleware得到的結果是[one, final]
,它們都是Generator函式,因此,執行下列語句的時候
// 由於fnMiddleware.call(ctx)語句,未傳入第二個引數,因此初次呼叫時候next為空,變成noop函式
if (!next) next = noop();
var i = middleware.length;
// 第一次 i 為 2
var i = middleware.length;
// i--返回值為2,i--之後i變為1
while (i--) {
next = middleware[i].call(this, next);
}
複製程式碼
第一次執行時,middleware[i]即是middleware[1],即final函式。此時,入參next為noop函式,返回的next,指向final中介軟體執行後所返回的Iterator物件。
而下一次迴圈發生時
// i--返回值為1,i--之後i變為0
while (i--) {
next = middleware[i].call(this, next);
}
複製程式碼
此時middleware[i].call(this, next);
,入參next,是final中介軟體返回的Iterator物件,即one中介軟體函式中的next引數
// 這個next引數是final中介軟體的Iterator噢
const one = function* (next){
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield next;
console.log('1-End');
}
複製程式碼
而返回的next則是one中介軟體函式執行所返回的Iterator。
再下一次迴圈發生時,此時迴圈不會發生
// i--返回值為0,i--之後i變為-1,不執行迴圈。
while (i--) {
next = middleware[i].call(this, next);
}
// 開始走return邏輯
return yield *next;
複製程式碼
此時next即為one中介軟體函式執行所返回的Iterator。
所以,koa-compose完成的工作,主要在於通過函式的組合,實現了next引數,即迭代器物件Iterator的傳遞。
4.3 關於yield *next
根據yield*關鍵字,yield *next相當於yield one中介軟體的程式碼
yield (
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield next;
console.log('1-End');
)
複製程式碼
但為什麼這裡要寫yield *next,直接yield next不好嗎?
個人認為,由於co函式會識別yield關鍵字後面的值的型別並轉化為Promise,即依次執行 toPromise函式 -> if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
,因此使用yield*和yield,在執行流程上沒有本質的區別。
但是,為什麼用yield_呢?我的理解是,由於最外層的next幾乎可以確定是一個Iterator,所以直接使用yield _,可以減少一層co函式的呼叫。
4.4 中介軟體的執行
由之前的分析可以知道,fnMiddleware,實際上相當於這樣的結構
co(function*(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
})
複製程式碼
所以,第一步,yield *next會幫助我們去等待next迭代器完成迭代,而這個next,就是one中介軟體的迭代器。而在one中介軟體裡,可以看到第一步是
console.log('1-Start');
const r = yield Promise.resolve(1);
複製程式碼
在co的自動執行流程中,會等著這個Promise完成,才會進行下一個yield。
而下一步,在one中介軟體中,則是
const t = yield next;
複製程式碼
前面提到,one中介軟體函式的入參next,是final中介軟體返回的Iterator
而co會識別到這個next是一個Iterator,進而在toPromise函式中走包裝Iterator為Promise的邏輯
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
複製程式碼
因此,在one中介軟體中執行yield next時,會等到final中介軟體完全執行完畢後,再回過頭來執行下一個yield語句。當然,one中介軟體已經沒有下一個yield語句了,因此它自身對應的next物件,也就執行完畢了。
用一段虛擬碼來描述,就是這樣的:
yield (
console.log('1-Start');
const r = yield Promise.resolve(1);
const t = yield (
console.log('final-Start');
this.body = { text: 'Hello World' };
console.log('final-End');
)
console.log('1-End');
)
複製程式碼
可以看到,這就是Koa所說的洋蔥圈模型:
如果我們把one中介軟體的程式碼改改
const one = function* (next) {
console.log('one-Start');
this.body = { text: 'Hello World' };
console.log('one-End');
}
複製程式碼
相當於在one中介軟體裡,並沒有通過yield next語句,來等待下一個中介軟體,也就是final中介軟體的執行完畢。因此可以說,next引數就是one中介軟體對於下一個中介軟體“是否執行”的控制權。
5 小結
至此,v1版本koa的中介軟體執行機制已經全部介紹完畢。
與Koa v2相比,Koa v1的流程控制方案是一致的,都是把下一個中介軟體的執行權,通過傳引數的方式,交給了當前中介軟體。
但不同的是,Koa v2傳遞的是包裝後的中介軟體函式本身,所以「下一個中介軟體的執行工作」,是當前中介軟體函式自己完成的。而Koa v1,則只是傳遞了迭代器物件Iterator,中介軟體函式只是描述了執行流程,具體的執行工作是交給Co工具庫來完成的。
而Koa v1的非同步邏輯,也是交給Co庫完成。它通過判斷迭代器執行next方法所返回的值的型別,並通過toPromise函式轉化成Promise型別的指,待該Promise被resolve時,再來下一次next方法執行的時機,進而實現了非同步邏輯。
關於我們
我們是螞蟻保險體驗技術團隊,來自螞蟻金服保險事業群(杭州/上海)。我們是一個年輕的團隊(沒有歷史技術棧包袱),目前平均年齡92年(去除一個最高分8x年-團隊leader,去除一個最低分97年-實習小老弟)。我們支援了阿里集團幾乎所有的保險業務。18年我們產出的相互寶轟動保險界,19年我們更有多個重量級專案籌備動員中。現伴隨著事業群的高速發展,團隊也在迅速擴張,歡迎各位前端高手加入我們~
我們希望你是:技術上基礎紮實、某領域深入(Node/互動營銷/資料視覺化等);學習上善於沉澱、持續學習;性格上樂觀開朗、活潑外向。
如有興趣加入我們,歡迎傳送簡歷至郵箱:shuzhe.wsz@alipay.com
本文作者:螞蟻保險-體驗技術組-漸臻
掘金地址:DC大錘