原文: lucasfe.cn
一、何为“代码嵌套”思想 ?
首先什么是”代码嵌套“思想呢?用语言描述来讲,就是构造一个有着嵌套逻辑的等待执行的函数构造器,这么说可能比较抽象,不太好理解,那我用reduce手写compose函数来带着大家领悟一下这种思想
function compose(...fn) {
return fn.reduce((prev, next) => {
return function(...args) {
return prev(next(...args))
}
})
}
如果不用compose方法,我们将要函数包函数的执行代码。只有两个函数看起来不是很繁琐,但是包装函数多了,就会很乱
// 代码使用:
function inputNumber(num) {
return Number(num);
}
function addUnit(num) {
return num + '元';
}
const result = inputNumber(addUnit(100)); // 100元
但是如果用compose函数,代码就会从树状结构变成了扁平的结构,这种方式也更容易拓展
const result = compose(addUnit, inputNumber)(100); // 100 元
从compose的实现上也可以看出来,主要方法就是构造一个嵌套的函数构造器,compose返回的构造器如下:
function(...args) {
return addUnit(inputNumber(...args));
}
二、抛出问题
- 什么是手动“代码嵌套”?
- 什么是自动”代码嵌套”?
三、CO源码
在当时还没有async await规范的时候,为了避免Promise回调地狱的出现,会用generator配合CO来解决
// CO的使用
const fs = require('fs').promises;
function* read() {
const a = yield fs.readFile('./a.txt', 'utf-8') ~// a = ‘b.txt’
const b = yield fs.readFile(a, 'utf-8')
return b;
}
co(read()).then(res => {
console.log(res); // 此时会返回
})
来手写一下CO方法!主要原理就是,返回一个Promise,无脑触发iterator的next方法,直到done的时候触发resolve
// it是传入的生成器函数,例如上面写的read函数
function co(it) {
return new Promise((resolve, reject) => {
function next(data) {
const { value, done } = it.next(data);
if(done) {
resolve(value);
return;
}
// 执行yield右边的函数
Promise.resovle(value).then(res => {
next(res);
}, reject);
}
// 第一次不需要给generator传值
next();
})
}
CO函数就是典型的自动“代码嵌套”,因为函数会无脑的触发next函数,而不是用户手动来触发。接下来我来介绍另一种方式,手动“代码嵌套”(又称用户执行“代码嵌套”)
四、Koa部分源码
Koa 是大名鼎鼎的 Node.js 的拓展库,Koa主要整合了Node.js http模块中比较鸡肋的地方。这篇文章主要介绍Koa中间件的一些实现原理,中间件的实践不深入讨论。
首先,介绍一下Koa中间件的用法:
const koa = require('koa')
const app = new koa();
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
await next()
console.log(4)
})
app.listen(3000); // 1 3 4 2
这段代码大家可以运行一下,输出后的结果应该是1 3 4 2。因为Koa中注册的中间件,会按照注册顺序进行执行,调用next方法会执行下一次注册的中间件。需要注意的是,尽量使用await next(),否则会执行顺序会错乱,例如:第二个注册函数中,next()之前有异步函数,这样第一个函数不会等待第二个函数整体执行完,结果会变成1 3 2 4
Koa中间件的实现:
const http = require('http');
class Koa {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
}
compose(ctx) {
function next(index) {
if(index === this.middlewares.length) return Promise.resolve();
// 执行注册的函数
// 传入的第二个参数,用于用户手动执行next函数
return Promise.resovle(this.middlewares[index](ctx, () => next(index + 1)))
}
return next(0)
}
handleRequest = (req, res) => {
// this.createContext方法就不写了,主要作用是创建上下文
const ctx = this.createContext(req, res);
// 主要看compose函数
this.compose(ctx).then(() => {
if (ctx.body instanceof Stream) {
ctx.body.pipe(res);
} else if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.statusCode = 404
res.end('Not Found')
}
})
}
// 服务链接成功之后,会触发this.handleRequest
listen(...args) {
const server = http.createServer(this.handleRequest);
server.listen(...args);
}
}
通过this.compose方法就实现了中间件,但是有些地方需要优化一下,比如一个函数多次调用next
app.use(async (ctx, next) => {
console.log(1)
await next()
// 多次调用,应该报错
await next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
await next()
console.log(4)
})
优化之后的this.compose
compose(ctx) {
let i = -1;
function next(index) {
// new
if(index <= i) return Promise.reject('不能多次调用next')
index = i
if(index === this.middlewares.length) return Promise.resolve();
// 传入的第二个参数,用于用户手动执行next函数
return Promise.resovle(this.middlewares[index](ctx, () => next(index + 1)))
}
return next(0)
}
五、总结
分析以及手写完CO和Koa的源码,可以清晰的发现它们都有着“代码嵌套”的思想,它们的不同点在于,CO的递归执行是内部黑盒执行的,而Koa的next方法是通过回调的形式,提供给用户来使用的。
今天分享的“阅读源码”的感悟就分享到这里了,我是一个热衷技术的小前端,github中也有一些积累的小项目,欢迎一起来搞事情