若是你使用過redux或者nodejs,那麼你對「中間件」這個詞必定不會感到陌生,若是沒用過這些也不要緊,也能夠經過這個來了解javascript中的事件流程。javascript
有一類人,很是的懶(好比說我),只有三種行爲動做,sleep
,eat
,sleepFirst
,僞代碼就是:php
var wang = new LazyMan('王大錘'); wang.eat('蘋果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2); //等同於如下的代碼 const wang = new LazyMan('王大錘'); wang.eat('蘋果'); wang.eat('香蕉'); wang.sleep(5); wang.eat('葡糖'); wang.eat('橘子'); wang.sleepFirst(2);
執行結果以下圖:java
無論什麼,先睡2Snode
而後作個介紹,吃東西,睡5Sgit
醒來,吃github
可是javascript只有一個線程,也並無像php的sleep
的那種方法。實現的思路就是eat、sleep、sleepFirst這些事件放在任務列中,經過next去依次執行方法。我仍是但願在看源碼前先手動實現一下試試看,其實這就是個lazyMan的實現。編程
下面是個人實現方式:redux
class lazyMan{ constructor(name) { this.tasks = []; const first = () => { console.log(`my name is ${name}`); this.next(); } this.tasks.push(first); setTimeout(()=>this.next(), 0); } next() { const task = this.tasks.shift(); task && task(); } eat(food) { const eat = () => { console.log(`eat ${food}`); this.next(); }; this.tasks.push(eat); return this; } sleep(time) { const newTime = time * 1000; const sleep = () => { console.log(`sleep ${time}s!`); setTimeout(() => { this.next(); }, newTime); }; this.tasks.push(sleep); return this; } sleepFirst(time) { const newTime = time * 1000; const sleepzFirst = () => { console.log(`sleep ${time}s first!`); setTimeout(() => { this.next(); }, newTime); }; this.tasks.unshift(sleepzFirst); return this; } } const aLazy = new lazyMan('王大錘'); aLazy.eat('蘋果').eat('香蕉').sleep(5).eat('葡萄').eat('橘子').sleepFirst(2)
咱們上面說過api
wang.eat('蘋果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2); //等同於如下的代碼 wang.eat('蘋果'); wang.eat('香蕉'); wang.sleep(5); wang.eat('葡糖'); wang.eat('橘子'); wang.sleepFirst(2);
若是你使用過過node,你會發現,這種寫法彷佛有點熟悉的感受,咱們來看一下一個koa2(一個node的框架)項目的主文件:數組
const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); const cors = require('koa-cors2'); const routers = require('./src/routers/index') const app = new Koa(); app.use(cors()); app.use(bodyParser()); app.use(routers.routes()).use(routers.allowedMethods()) app.listen(3000);
有沒有發現結構有一點像?
廢話很少說,直接看源碼...
app.use就是用來註冊中間件的,咱們先看use的實現:
use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); 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); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }
先解釋一下里面作了什麼處理,fn
就是傳入的函數,首先確定要判斷是不是個函數,若是不是,拋出錯誤,其次是判斷fn
是不是一個GeneratorFunction
,我用的是koa2,koa2中用async
、await
來替代koa1中的generator
,若是判斷是生成器函數,證實使用或者書寫的中間件爲koa1的,koa2中提供了庫koa-convert
來幫你把koa1中的中間件轉換爲koa2中的中間件,這裏若是判斷出是koa1的中間件會給你提醒,這裏會主動幫你轉換,就是代碼中的convert
方法。若是驗證沒出現問題,就註冊這個中間件並放到中間件數組中。
這裏咱們只看到了把中間件加到數組中,而後就沒有作其餘處理了。
咱們再看koa2中listen
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
這裏只是啓動了個server,而後傳進了一個回調函數的結果,咱們看原生啓動一個server大概是什麼樣的:
https.createServer(options, function (req, res) { res.writeHead(200); res.end("hello world\n"); }).listen(3000);
原生的回調函數接受兩個參數,一個是request
一個是response
,咱們再去看koa2中這個回調函數的代碼:
callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { res.statusCode = 404; const ctx = this.createContext(req, res); const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fn(ctx).then(handleResponse).catch(onerror); }; return handleRequest; }
這裏有一個const fn = compose(this.middleware);
,compose
這種不知道你們用的多很少,compose
是函數式編程中使用比較多的東西,這裏將多箇中間件組合起來。
咱們去看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!') } /** * @param {Object} context * @return {Promise} * @api public */ 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, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } }
首先判斷是不是中間件數組,這個不用多說,for...of是ES6中的新特性,這裏不作說明,須要注意的是,數組和Set集合默認的迭代器是values()方法,Map默認的是entries()方法。
這裏的dispatch和next同樣是全部的中間件的核心,dispatch的參數i其實也就是對應中間件的下標,,在第一次調用的時候傳入了參數0,若是中間件存在返回Promise
:
return Promise.resolve(fn(context, function next () { return dispatch(i + 1) }))
咱們lazyMan鏈式調用時不斷的shift()
取出下一個要執行的事件函數,koa2裏採用的是經過數組下標的方式找到下一個中間件,這裏是用Promise.resolve
包起來就達到了每個中間件await next()
返回的結果都恰好是下一個中間件的執行。不難看出此處dispatch
是個遞歸調用,多箇中間件會造成一個棧結構。其中i
的值老是比上一次傳進來的大,正常執行index
的值永遠小於i
,但只要在同一個中間件中next
執行兩次以上,index
的值就會等於i
,同時會拋出錯誤。但若是不執行next
,中間件的處理也會終止。
整理下流程:
compose(this.middleware)(ctx)
默認會執行中間件數組中的第一個,也就是代碼中的dispatch(0)
,第一個中間件經過await next()
返回的是第二個中間件的執行。await next()
,而後返回第三個...以此類推context
來對請求做處理了。