JavaScript设计模式之责任链模式
概念
在《JavaScript设计模式与开发实践》中是这样介绍责任链模式的:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链处理该请求,知道有一个对象处理它为止。
责任链模式案例
接下来,我们通过一个常见的案例来说明到底什么是责任链模式,以及为什么要用责任链模式。
案例描述
我们需要做一个售卖手机的电商网站,经过缴纳500元或200元定金后,开始进入购买阶段。而电商网站有如下的优惠模式:
- 支付过500元定金的用户: 收到100元优惠券
- 支付过200元定金的用户: 收到50元优惠券
- 没有支付定金的用户: 普通购买,没有优惠券,且库存有限的情况下不一定能买得到
在页面加载时,我们会得到以下几个字段:
orderType
: 订单类型,1: 500元定金用户; 2: 200元定金用户; 3: 普通用户;pay
: 是否已经支付了定金,ture
表示已经支付定金,虽然下单是500元用户,但是如果没有支付定金,也只能按照普通用户购买;stock
: 普通购买的手机数量库存,已经支付定金的将不会收到此限制;
原始代码
看到上诉需求后,相信大家肯定是可以完成该功能,无非就是if - else
没有什么逻辑是解决不了的,接下来可能会写出以下的代码:
var order = function(orderType, pay, stock) {
if (orderType === 3 || pay === false) {
// 普通购买用户
if (stock > 0) {
console.log('普通购买,无优惠券')
} else {
console.log('手机库存不足')
}
} else if (orderType === 1) {
console.log('500元定金用户,100元优惠券')
} else if (orderType === 2) {
console.log('200元定金用户,50元优惠券')
}
}
我对书中的原始代码做了优化,因为感觉书中的原始代码是写的过于复杂了,当然也可能是为了更好的与业务逻辑吻合,先判断orderType,再判断是否支付定金的方式更符合业务逻辑,下面还是贴出来书中的原始代码:
var order = function (orderType, pay, stock) {
if (orderType === 1) { // 500 元定金购买模式
if (pay === true) { // 已支付定金
console.log('500 元定金预购, 得到 100 优惠券');
} else { // 未支付定金,降级到普通购买模式
if (stock > 0) { // 用于普通购买的手机还有库存
console.log('普通购买, 无优惠券');
} else {
console.log('手机库存不足');
}
}
}
else if (orderType === 2) { // 200 元定金购买模式
if (pay === true) {
console.log('200 元定金预购, 得到 50 优惠券');
} else {
if (stock > 0) {
console.log('普通购买, 无优惠券');
} else {
console.log('手机库存不足');
}
}
}
else if (orderType === 3) {
if (stock > 0) {
console.log('普通购买, 无优惠券');
} else {
console.log('手机库存不足');
}
}
};
虽然我们实现了需求,但是这样的代码显得更难以阅读,当功能不断迭代,优惠的逻辑越来越复杂的时候,我们的代码最终会变成难以维护的“屎山”,尽管我自己写的代码更少一点,随着优惠逻辑越来越复杂最终也会难以维护。
责任链模式优化
接下来,我们将用责任链模式对这段代码进行优化:
- 将功能分为三个单独的函数:500元定金函数
order500
、200元定金函数order200
、无定金函数orderNormal
- 将
orderType
、pay
、stock
当作参数分别传给这三个函数 - 依次通过各个函数,直到可以处理为止,即:
order500
不满足时,传递给order200
, order200不满足时传递给orderNormal
接下来写一下代码来实现这个功能:
var order500 = function(orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元用户,得到100元优惠券')
} else {
order200(rderType, pay, stock)
}
}
var order200 = function(orderType, pay, stock) {
if (orderType === 2 && pay) {
console.log('200元用户,得到50元优惠券')
} else {
orderNormal(orderType, pay, stock)
}
}
var orderNormal = function(orderType, pay, stock) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
order500(1, true, 500)
优化责任链传递
根据上面的代码,我们将不同的逻辑拆分成了3个互不关联的函数,但是大家也不难发现,处理链条传递的过程非常的僵硬,如果某一天有先后顺序的调整,这样的代码也是很难维护的,这也违反了开放-封闭原则。所以我们需要对个代码进行优化,让他变得更加灵活。
- 首先,我们对于函数的传递进行一个约定:当不满足条件时,返回固定的字符串
nextSuccessor
表示继续向后传递 - 接下来把函数包装进责任链节点
- 最后指定节点在责任链中的顺序
代码如下:
将三个函数中需要向后传递的部分调整为返回nextSuccessor
var order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元用户,得到100元优惠券')
} else {
return 'nextSuccessor'
}
}
var order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay) {
console.log('200元用户,得到50元优惠券')
} else {
return 'nextSuccessor'
}
}
var orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
封装了一个Chain
方法,用来将之前的三个函数进行包装,setNextSuccessor
函数用来指定责任链中的下一个节点,而passRequest
函数则用来一次执行各个责任链中的节点,直到找到可以处理的函数。
var Chain = function (fn) {
this.fn = fn;
this.successor = null;
}
Chain.prototype.setNextSuccessor = function (successor) {
return this.successor = successor;
}
Chain.prototype.passRequest = function () {
var ret = this.fn.apply(this, arguments);
if (ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
}
下面,封装三个节点,并指定它们的顺序:
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
// 指定顺序
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
// 执行
chainOrder500.passRequest(1, false, 50)
使用责任链模式优化后的代码,虽然看起来更多,但更利于后续的维护,我们只需要关注各个小函数的逻辑,如果需要新增一个300元优惠券的逻辑,我们也只需要创建一个order300
的函数,然后封装成节点chainOrder300
,设置其顺序chainOrder500.setNextSuccessor(chainOrder300)
。
异步责任链
根据上面的代码,我们可以实现通过函数中返回固定字符串nextSuccessor
来表示是否需要传递给下一个节点,而在实际的业务中,我们可能经常会遇到异步问题。比如在节点中发起一个ajax
请求,根据请求结果才能判断是否继续在责任链中passRequest
传递给下一个节点。
所以,我们需要再新增一个next
方法,手动传递给责任链下一个节点:
Chain.prototype.next = function() {
return this.successor && this.successor.passRequest.apply( this.successor, arguments );
}
假设,order500
需要延迟一秒后再传递给下一个节点
var order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元用户,得到100元优惠券')
} else {
setTimeout(() => {
this.next(orderType, pay, stock);
}, 1000);
}
}
chainOrder500.passRequest(1, false, 50)
效果如下:
可以看到异步功能也实现了。
源码中的责任链模式
express中间件
回顾中间件
在Express
官网中是这样介绍中间件的:
中间件函数能够访问请求对象(req
)、响应对象(res
)以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next
的变量来表示。如果当前中间件函数没有结束请求/响应循环,那么它必须调用 next()
,以将控制权传递给下一个中间件函数。否则,请求将保持挂起状态。
写一个文档中的例子来回顾一下中间件的作用:
var app = express();
app.use(function (req, res, next) {
console.log('Time:', Date.now());
next();
});
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method);
next();
});
app.get('/user/:id', function (req, res, next) {
res.send('USER');
});
上面的代码中,第一个中间件会在每次收到请求时执行,第二个中间件会在 /user/:id
路径中为任何类型的 HTTP 请求执行,第三个则会针对处理 /user/:id
的GET
请求。对于前两个中间件,需要通过next函数依次向下执行,这样的执行逻辑与我们这次介绍的责任链模式的非常相似。那么,我们接下来就看下Express
中是如何实现这样的逻辑的。
中间件原理
为了便于了解express
中next
的实现原理,我们写一个简单的demo进行调试,代码如下:
var express = require('express');
var app = express();
app.use(function (req, res, next) {
next();
})
app.get('/search', function (req, res) {
res.send({
query: req.query,
params: req.params,
});
});
app.listen(3000);
执行express, express初始化
当我们执行express
函数时,首先express
会进行一个初始化,源码如下:
// /lib/express.js
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
在研究中间件实现的过程中,初始化部分的源码并不重要,只需要知道当服务接收到一个请求的时候,实际会去执行handle
函数即可。
执行app.use,初始化Router
当我们执行app.use
时,会执行lazyrouter
函数创建一个Router
实例,接下来会遍历use
中的函数,并调用router.use
为Router
添加中间件,
lazyrouter
源码如下:
// lib/application.js
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};
router.use
源码如下:
// lib/router/index.js
proto.use = function use(fn) {
var offset = 0;
var path = '/';
var callbacks = flatten(slice.call(arguments, offset));
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
};
在源码中可以了解到,当我们执行app.use
时,会做以下几件事情:
-
当我们使用
app.use
创建中间件时,express
源码中会将函数构建成一个Layer
实例添加stack
中 -
初始化Router实例后,express默认创建了两个的
Layer
实例存入stack
中
执行app.get,注册中间件
当我们执行app.get
函数时,在express
内部实际执行了router.route
函数,最终会创建一个Layer
实例存入stack
中, 源码如下:
在express
中对各种类型的请求添加了函数:
// lib/application.js
methods.forEach(function(method){
app[method] = function(path){
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
通过源码可以看到,内部会调用router.route
函数,接下来再了解一下这部分的源码:
// lib/router/index.js
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
此时,可以看到,在route
函数中,express
会将函数构造化成一个Layer
实例,并存入stack
中。
到这里,demo服务算是启动完成了,此时我们可以通过调试看到,stack
中存在4个Layer
,分别是内部的两个和demo代码中app.use
和app.get
函数执行后的两个,如图所示:
处理请求,中间件实现原理
接下来,我们再看一下当express
接收到一个请求时,内部会如何处理。这部分逻辑也是了解express
中间件原理的核心,当我们收到一个请求时,此时会执行app.handle(req, res, next)
函数,app.handle
源码如下:
// lib/application.js
app.handle = function handle(req, res, callback) {
var router = this._router;
router.handle(req, res, done);
};
在这段代码中,我们可以看到done函数实际就是我们demo代码中的next
函数,接下来会调用到router.handle
函数,我们再看下router.handle
函数的源码,由于这部分源码较多,我会将一部分用不到的逻辑省略到,接下来是删减后的源码部分:
// lib/router/index.js
proto.handle = function handle(req, res, out) {
var self = this;
var idx = 0;
var stack = self.stack;
req.next = next;
next();
function next(err) {
var layerError = err === 'route'
? null
: err;
// find next matching layer
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
if (match !== true) {
continue;
}
if (!route) {
// process non-route handlers normally
continue;
}
}
// no match
if (match !== true) {
return done(layerError);
}
// store route for dispatch on change
if (route) {
req.route = route;
}
// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
next(layerError || err)
} else if (route) {
layer.handle_request(req, res, next)
} else {
trim_prefix(layer, layerError, layerPath, path)
}
});
}
}
在源码中可以看到最重要的部分就是需要执行next
函数,下面我们要将next
函数分开几个部分分别来看:
- 首先,在
next
函数中有个while
循环:
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
if (match !== true) {
continue;
}
if (!route) {
// process non-route handlers normally
continue;
}
}
在这段精简后的逻辑中,我们可以看到,这个循环就是在stack
中找到第一个匹配的layer
,当match
为true
也就是匹配到的时候,则会跳出循环执行后面的逻辑。带入到我们的demo案例中,当我们发起了一个/search
的请求时,实际在刚才stack
中的四个layer
都满足条件,由于前面两个是express
内部的中间件,我们就从第三个layer
的逻辑来思考。
- 当跳出循环后,我们会进入到后面的逻辑,源码如下:
// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
next(layerError || err)
} else if (route) {
layer.handle_request(req, res, next)
} else {
trim_prefix(layer, layerError, layerPath, path)
}
});
这部分逻辑中,我们可以看到就是对params
进行了赋值后,执行process_params
函数。而在我们的demo代码中,process_params
直接执行了done
,也就是执行process_params
的最后传入的回调函数,根据前面stack中的四个Layer截图,我们发现只有最后一个Layer是有route的,也就是说前面几个中间件执行时route
实际为undefined
,所以将会执行trim_prefix
函数。
最终函数会执行到layer.handle_request(req, res, next)
, layer.handle_request
的源码如下:
// lib/router/layer.js
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
我们可以看到,接下来我们将会执行fn
,也就是demo
中传入的中间件函数,当我们在代码中调用next
函数时,此时执行的是handle中创建的next函数,并从下一个Layer
开始(stack[id++]
)继续遍历找到满足条件的Layer
并执行。这样在各个中间件中调用next
时,实际就是不断从stack
中取出下一个Layer
,满足执行条件就会跳出循环执行这个中间件
原理总结
根据刚才的源码分析,我们总结出为什么当我们调用next
函数时会自动执行到下一个中间件函数:
- 首先,我们将所有的函数都添加到
stack
中,每一个函数为一个Layer
实例- 在调用时执行handle函数,handle内部创建一个next函数,通过next函数获取stack中的下一个满足执行条件的Layer
- 当中间件函数主动调用next函数时,会找到下一个满足条件的Layer并执行(
layer.handle_request(req, res, next)
),依次类推直到全部执行完成
以上就是对express中间件实现方式的源码分析,可以发现通过调用next
控制执行下一个中间件的逻辑是比较简单。
总结
合理的使用责任链模式可以很好的帮助我们管理代码,降低发起请求的对象和处理请求对象之间的耦合性。责任链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。 在前端领域中,也有很多责任链模式的影子,例如:作用域链、原型链、DOM节点中的事件冒泡。 希望大家可以通过前面的案例和
express
中间件的实现原理对责任链模式有一个更深刻的认识,能够在遇到类似场景时想到使用责任链模式。不论是使用案例中setNextSuccessor
初始化执行顺序还是像express
源码中那样通过一个stack
规定执行顺序,只要适合自己的场景那就是最好的。
感谢阅读🙏
转载自:https://juejin.cn/post/7268622342728335412