逐行分析Koa v1 中間件原理

0.前言

上一篇文章裏,已經對v2版本的koa中間件原理作了逐行分析,講清楚了它的流程控制和異步方案。javascript

可是,仍然有大量的基於koa的項目、框架、庫在基於v1版本的koa在工做,而它的中間件是Generator函數,其運行機制與v2版本的koa中間件有比較大的不一樣。前端

所以,有必要解釋清楚v1版本的koa中間件原理,做爲對上一篇文章的補充,但願能對那些仍然在項目中使用v1版本koa的同行同窗有所幫助。java

PS:本文基於v1.6.2的koa源碼,結構圖以下:node

1.響應機制

本段內容與上一篇文章大體相同,已經看過該文章的話,能夠跳過這一節git

上一篇文章同樣,先從一個簡單的Demo開始,來看Koa的使用方式。es6

const Koa = require('koa');
const app = new Koa();
複製代碼

Koa變量指向是什麼呢?咱們知道:github

require在查找第三方模塊時,會查找該模塊下package.json文件的main字段。json

查看koa倉庫目錄下下package.json文件,能夠看到模塊暴露的出口是lib目錄下的application.js文件api

{
  "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大錘

相關文章
相關標籤/搜索