逐行分析Koa v1 中介軟體原理

螞蟻保險體驗技術發表於2019-03-18

0.前言

上一篇文章裡,已經對v2版本的koa中介軟體原理做了逐行分析,講清楚了它的流程控制和非同步方案。

但是,仍然有大量的基於koa的專案、框架、庫在基於v1版本的koa在工作,而它的中介軟體是Generator函式,其執行機制與v2版本的koa中介軟體有比較大的不同。

因此,有必要解釋清楚v1版本的koa中介軟體原理,作為對上一篇文章的補充,希望能對那些仍然在專案中使用v1版本koa的同行同學有所幫助。

PS:本文基於v1.6.2的koa原始碼,結構圖如下:

逐行分析Koa v1 中介軟體原理

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所說的洋蔥圈模型:

逐行分析Koa v1 中介軟體原理

如果我們把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大錘

相關文章