听说你用过koa?写一下compose吧

听说你用过koa?写一下compose吧

这段时间也面试挺多家的了,基本都是二面挂😂。深感找工作之不易,心累,同时也认识到自己的很多不足,要继续努力学习。

在 CVTE 二面的时候,面试官看我简历上面写着玩过 koa,然后就出了下面这道题,瞬间懵比…没想到我简历只写了个了解Node.js,也会被问及有关源码的东西,虽然看过一点,但也忘得七七八八了,挺虚的。不说了,先看题吧。

其实compose函数可以说是 koa 中间件的核心了(koa 核心代码相当精简,叹为观止,连我这渣渣都能看个大概😂)。

题目

题目大概如下,叫我实现 compose 函数,以至于得到预期的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(next) {
console.log('hello');
next();
console.log('end foo');
}
function bar(next) {
console.log('world');
next();
console.log('end bar');
}
var middleware = [foo, bar];
function compose() {
// do something here
}
compose(middleware)();
// ->
// 'hello'
// 'world'
// 'end foo'
// 'end bar'

持续懵比

即使心虚也要硬着头皮上了,记得当时有点紧张,以至于感觉耳朵在发热,尴尬。

  • 当看到compose(middleware)()的时候,就可以知道 compose 函数要返回一个 function 了:

    1
    2
    3
    4
    5
    6
    function compose(middleware) {
    // do something here
    return function() {
    // do something here
    };
    }
  • 其次,要像 koa 那样将各个中间件函数收尾连接起来的话,next 函数是个关键点,调用 next 就会执行下一个函数。

想到这,我就开始考虑怎么把下一个函数传给当前函数的 next 了,然后写出了下面的代码:

1
2
3
middleware.forEach((fn, index) => {
fn(middleware[index + 1]);
});

233,当时写完后看了两眼,我就赶紧删掉了,脑补面试官内心:这孩子脑子肯定秀逗了😂…然后,我就挂在这里了。

事后回顾了一下 koa-compose 的源码,发现我的想法太粗暴了!上面的 next 的确是指向了下一个中间件函数,但是 forEach 的每个函数都会再执行一遍,但是中间件的要求是下一个函数只能通过 next 来触发。这样的话,可以考虑中间件函数的动态注入了,而不是一次性注入。

也就是说每次 next 的时候,都要进行一次注入,将下一个中间件函数注入到即将被调用的中间件函数中。然而如果还是像上面这样fn(middleware[index + 1]),显然是不行的,因为 index 是需要动态改变的。

其实仔细一看,就会发现 koa 的中间件模式有点递归的味道!看图:

信了吧?那是不是意味着可以用递归的方式来实现中间件模式呢?这个思路是正确的,但还存在一个问题,各个中间件函数是不同的,并且已经是给定的,留给我们把控的只有 next 参数,所以 next 应是突破口了。

综上,

  • 下一个中间件函数需要动态注入,则每个中间件都必须知道自己的位置i,这样下一中间件就是i + 1的事情了;
  • 递归思想。既然不能直接修改各个中间件,那么可以在中间件函数外再包一层函数,边界的控制由这个函数实现,递归的进行由 next 实现。

看图最清晰明了:

可以看到,递归的进行实际上仍然是由 next 负责的,外层函数只是做一些边界控制的工作。

其实这层外嵌的函数就相当于一个闭包,继而我们可以将每个中间件的位置i存储在这个闭包当中。也正由于闭包的存在,递归才得以进行下去。关于闭包的问题,可以看下我之前写的JS的作用域链

撸码了

先动手实现这个外嵌的函数:

1
2
3
4
5
6
7
8
9
function inject(i) {
var fn = middleware[i];
if (!fn) {
return;
}
return fn(function() { // 这个函数就是每个中间件的 next
return inject(i + 1); // 进行递归
});
}

寥寥几行,包含的信息可不少啊。实现了 next 函数的注入之后,一切都好办了,完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function compose(middleware) {
if (!Array.isArray(middleware)) {
return;
}
for (const fn of middleware) {
if (typeof fn !== 'function') {
return;
}
}
function inject(i) {
var fn = middleware[i];
if (!fn) {
return;
}
return fn(function() {
return inject(i + 1);
});
}
return function() {
return inject(0);
}
}

当然,实际的 koa-compose 代码并不仅仅如此,因为 koa 的中间件都要是 async 函数,next 的调用需要加上 await。不过思想是不变的,只是将某些地方改为 Promise 而已。

叹为观止!