-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Koa之中间件执行解析 #139
Labels
Comments
👍 |
由浅入深,层层递进,赞 |
图文并茂 |
深入浅出,抛出疑问、解答,很棒~ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
title: Koa之中间件执行解析
date: 2017-10-25
从14年开始接触 Koa,翻过源码,写过文章,后面也陆续用 Koa 做过一些项目,但一直都没系统性的学习总结过。今天想通过这篇文章,给大家介绍下 Koa 中中间件从加载到执行的整个过程剖析。如有不准确的地方还忘指出。
发展历程
我们先来看下 Koa 的整个发展历程,每个里程碑都发生了哪些变化
纵观整个发展历程,我们似乎发现个规律,Koa 每两年发布一个大版本。那么,我们是不是可以期待下2019年迎来 v3 呢?
知识点回顾
在正式分析 Koa 源码之前,我们还需要一些其他知识的储备。这里我们就来简单回顾下 Generator 函数、Co & Promise 的使用。如果想深入学习的,可以网上找找相关的资料来学习。如果你已熟练掌握它们的用法,可以直接跳过下面内容,继续阅读后面内容。
generator 是 ES6 中处理异步编程的解决方案,我们通过一个简单的例子,来回顾下它的用法。
看完例子,我们需要注意几个点:
通过上面的例子我们可以看到,通过不断的调用 next 方法,可以执行完整个 generator function。那有没有什么解决方案,可以自动的调用这些 next 方法呢?答案就是 co。另外,Promise 作为 ES6 中提供的解决异步编程的方案,可以有效的规避 callback hell 的产生。这里同样通过一个小例子,来回顾下 co 的实现原理以及 Promise 的用法。
这个例子
res
直接输出了[1, 2, 3]
。那我们来看下,co 内部到底做了什么操作,直接上代码可以看到 co 最终返回的是一个 Promise 对象,所以才有了例子中的 catch 方法,这个先不管。我们来看下 Promise 内部的具体实现
首先判断这个
gen
是不是一个function
,如果是就直接调用;再通过判断是否有 next 方法,来判断是不是一个 generator 实例,如果不是就直接resolve
返回;在函数onFulfilled
内部第一次调用gen.next
方法,ret 的值{value: 数组,done: false}
,再把 ret 传给内部的 next 方法;因为我们知道ret.value
的值是一个数组,所以我们直接来看arrayToPromise
这个方法。大家先回忆下 Promise 的用法,
Promise.resolve()
可以创建一个 Promise 实例,Promise.all()
用于将多个 Promise 实例,包装成一个新的 Promise 实例。只有数组中的每个实例状态都变成fulfilled
的时候,Promise.all
的状态才会是fulfilled
,此时数组中每个实例的返回值组成一个数组,传递给all
的回调函数。因为我们例子中用的都是
Promise.resolve()
,所以我们Promise.all
的状态肯定是fulfilled
。回头看next
方法中的此时的 value 值是
Promise.all
包装成的新实例,且状态是fulfilled
,调用then
的第一个回调函数onFulfilled
,参数是有各个子实例返回值组成的数组,也就是[1, 2, 3]
。所以到第二次调用gen.next
方法的时候,res
的值是数组[1, 2, 3]
。通过前面 generator 中的回顾,我们知道,传给gen.next
的参数会赋值给外部 yield 左侧的变量,所以上述例子中,res
最终输出[1, 2, 3]
。而此时我们内部的onFulfilled
中,ret
的值为{value: undefined, done: true}
,在 next 方法中直接 resolve 返回,结束运行。登堂入室
有了上面的这些基础打底,终于可以来讲讲正题了。老规矩,我们先来看下 Koa 中如何添加中间件。
在这里,我们添加了两个中间件,先不着急知道代码的输出结果,当你第一次看到这段代码的时候,会不会有疑问?
那么我们就带着这些问题,来看下 Koa 内部是怎么实现的,为方便理解,删去了部分代码
由代码可知,我们通过
use
方法添加的中间件,都被塞到了一个事先定义好的middleware
数组中。通过app.listen
入口方法,创建了一个 http server 服务。在callback
回调中,我们着重来看下这行代码这是嵌套了两层方法,第一层由中间件组件的
middleware
数组被传入了compose
方法中;第二层是由第一层返回的结果传给了co.wrap
方法。那我们先来看第一层的compose
方法返回一个 generator 函数 ,在函数内部,
i
是数组长度,middleware[i]
表示数组最末尾一个中间件,执行middleware[i].call(this, next)
生成一个 generator 实例并赋值给next
。大家注意下这里的参数next
,第一次next
为noop
这个空的 generator function。做完一次i--
后,next
成了上一个 generator 函数的实例对象,以此类推。换句话说,这个while
循环从最后一个中间件开始处理,一直往前,把后一个 generator 函数的实例作为前一个 generator 函数的参数传入,那么执行完整个循环,我们的next
的值就是数组第一个 generator 的实例。看到这,我们是不是可以解答上面第一个疑问了呢,参数
next
其实就是下一个中间件的 generator 实例。揭开面纱
compose 的存在,让整个中间件都串联了起来,但它并没有让中间件跑起来。要让整个过程跑起来,关键还是要看 co。我们继续来看下面的代码
这里的
fn
就是我们上面 compose 返回的 generator 函数。在wrap
内部调用co
,继续我们 co 内部的分析,在函数onFulfilled
内部第一次调用gen.next
方法,执行到 compose 内部的yield *next
,next 是我们的第一个中间件,根据 delegating yield 的知识,它会代理到第一个中间件的内部去,在我们中间件的yield next
处暂停,此处next
是下一个中间件的 generator 实例。ret
的值是{value: generator实例, done: false}
,再把 ret 传给内部的 next 方法。如果 value 值是 generator 函数或者是 generator 实例,则继续调用 co。在函数
onFulfilled
内部第二次调用gen.next
方法到第二个中间的yield next
处暂停。以此类推,直到最后遇到那个空的 generator 函数noop
为止,执行if (ret.done) return resolve(ret.value)
,promise 状态置为fulfilled
。因为每次调用 co 都是返回一个 promise 实例,且
ret.done
为 true 的时候,状态被置为fulfilled
,所以执行回调中的onFulfilled
函数。这样又从最后一个中间件往回执行,像个回形标一样,整个流程串了起来。看完这部分,我们再来解答下剩下的两个疑问,
yield next
用于执行下一个中间件的内容;中间件之间的执行顺序也是按照use
的顺序来执行的。是不是觉得整个过程很绕呢,但又不得不佩服作者设计的巧妙。我们可以从下图更直观的理解整个执行过程
知识点扩展
到此为止,中间件的整个执行过程都已经讲解完了。不过大家在看 compose 源码的时候,有没有一个疑惑呢,为什么在 compose 内部是
yield *next
,而在我们的中间件里都是yield next
呢?按理说这里的 next 都是 generator 实例啊,有什么区别?Koa 的维护者们也曾讨论过这个问题,具体看查看最后两个参考链接。性能上来说,
yield *next
稍微优于yield next
,毕竟前者是原生的,后者是需要经过 co 包装处理过的,但写成后者也没什么大影响,Koa 作者 TJ 本人也是很反对两者之间切换来写的,推荐使用后者。再者,yield *
后面只能跟 generator 函数或者是可迭代的对象,而中间件中的yield
我们可以跟function
,promise
,generator
,array
或者object
,因为 co 最终都会帮我们处理成 promise,所以建议大家在用 Koa 做开发的时候,都能写成yield next
。参考
yield* next
instead ofyield next
koajs/compose#2by 贾克斯
The text was updated successfully, but these errors were encountered: