本文将介绍javascript中常用的设计模式原理和实现,并结合实例讲解其应用。
本篇文章先介绍单例模式,策略模式,代理模式,发布订阅模式和命令模式,其它几种模式后续文章将继续介绍。
单例模式就是一个实例在整个网页的生命周期里只创建一次,后续再调用实例创建函数的时候,返回的仍是之前创建的实例。在实际开发中应用十分广泛,例如页面中的登录框,显示消息的提示窗,都只需要一个实例即可。
我们以消息提示框为例,展示如何实现单例模式。代码如下:
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>singleTontitle>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* 提示框动画和样式 */
@keyframes appear{
from {
left: 100%;
}
to {
left: 50%;
}
}
.message-tip{
animation: appear .5s linear;
position: fixed;
width: 200px;
background: red;
color: #fff;
line-height: 1.5;
padding-left: 5px;
top: 20px;
left: 50%;
border: 1px solid #d3d3d3;
transform: translateX(-50%);
}
style>
head>
<body>
<button id="tipsBtn" onclick="showMsg()">提示消息button>
<script>
//创建提示框单例
let createTips = (function(){
let tipDom = null;
function createDom() {
let div = document.createElement('div');
div.setAttribute('class', 'message-tip');
return div;
}
return function(msg) {
if (!tipDom) {
tipDom = createDom();
}
tipDom.innerHTML = msg;
return tipDom;
}
})();
//验证是够是单例,打印true,则是单例模式
console.log(createTips('1') === createTips('2'));
function showMsg() {
let tips = createTips('这是消息提示');
document.body.appendChild(tips);
setTimeout(() => {
document.body.removeChild(tips);
}, 5000);
}
script>
body>
html>
代码讲解:使用createTips函数创建的div时,如果原先已经创建了div,则直接调用原来的div,不再重新创建。除了提示框,网页中一些登录框等等也可以用单例模式实现,网页的整个生命周期里,只有一个dom被创建。或者js的代码中的某个实例如果共享的,只需要一次,就使用单例模式创建。window对象也是一个单例,整个生命周期里只有一个window对象。
策略模式是指将策略(算法)封装起来,策略的目的是将算法和使用分离开。
我们假设有这样一个应用:
年终绩效评定,有S,A,B,C四种绩效,对应年终奖为4,3,2,1个月工资,设计函数,计算绩效。函数如下:
function calBonus(grade, salary) {
if (grade === 'S') {
return salary*4;
} else if (grade === 'A') {
return salary*3;
} else if (grade === 'B') {
return salary*2;
} else {
return salary;
}
}
函数里各种判断条件,如果算法实现比较复杂,这个函数会异常庞大。我们用策略模式改造函数可如下:
//计算绩效为S的年终
function S(salary) {
return salary * 4;
}
//策略使用
function calBonus(fn, salary) {
return fn(salary);
}
//计算绩效为S,工资为20000的年终
calBonus(S, 20000);
这种方法改写代码后,代码的扩展性就好了一些,实际的算法由函数封装起来,代码的扩展性强了,也方便复用。
这就是策略模式的应用之一,在代码中,将具体的算法或策略封装起来,和使用的场景分离,可以提高代码扩展性和复用率。
代理模式很好理解,我们不能直接使用目标函数,而是通过调用代理函数来实现对目标函数的使用。
对于网络代理这个词语,每个同学应该都了解,就是无法直接上网,把上网的请求都发送到代理服务器,由代理服务器请求数据,然后转发给相应人员。
现在有一个场景,某个网页有很多请求,有部分请求是不被允许的,我们使用代理模式实现这一功能。代码如下:
//允许的请求地址
let urlArr = ['/aaa', '/bbb'];
//正常的ajax请求函数,url请求地址,method请求方法, data请求数据,callback回调函数
function commonAjax(url, method, data, callback) {
//具体实现略过,这个大家应该都知道
}
//代理请求函数
function proxyAjax(url, method, data, callback) {
if (urlArr.indexOf(url) > -1) {
commonAjax(url, method, data, callback);
} else {
let data = {
status: false,
message: '该请求不被允许',
code: 403
}
callback(data);
}
}
代码解析:urlArr是我们允许的请求地址,其它请求不允许发送,commonAjax是正常的请求函数,proxyAjax是代理请求函数。
注意:代理接口和实际函数接口要保持一致。
后续需求发生变化,不需要限制任何请求,直接调用请求函数即可。现在大部分公司联网都是有限制的,过滤一部分网站,只有公司白名单里的网站才能访问,其功能和上述代码逻辑是一致的。
发布订阅模式在实际应用中非常常见,例如,我们在微信App上关注了某个公众号,当该公众号有新文章发布时,就会通知我们。
发布订阅模式定义了一种一对多的依赖关系,当“一”发生变化,通知多个依赖。
为了好理解,先说明几个名词含义,我们以微信关注公众号为例讲解,如下:
subscriber: 订阅者,我们关注了公众号,我们就是订阅者。
subject:主题,微信公众号的唯一。
publisher: 消息发布者,公众号的维护者,不断发布新的文章。
当然还有一个调度中心,这样就是完整的发布订阅模式了。
现在我们就实现发布订阅模式。代码如下:
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>发布订阅模式实例title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.main{
display: flex;
}
.msg-item{
width: 300px;
border-right: 3px solid #d3d3d3;
min-height: 400px;
}
style>
head>
<body>
<div class="main">
<div class="msg-item">
<div>
<input type="text" id="subject" placeholder="输入订阅主题" style="width: 150px">
<button onclick="subscribe()">订阅消息button>
div>
<h3>已订阅消息列表h3>
<ul id="subscribe_list">
ul>
<h3>接收到消息列表h3>
<ul id="message_list">
ul>
div>
<div class="msg-item">
<input type="text" id="pub_subject" placeholder="输入发布主题" style="width: 150px">
<input type="text" id="pub_message" placeholder="输入发布消息" style="width: 200px">
<button onclick="publishMsg()">发布消息button>
<h3>发布消息列表h3>
<ul id="publish_list">
ul>
div>
div>
<script>
//调度中心算法实现
function pubsub() {
//主题列表
this.subjects = {};
};
//发布消息
pubsub.prototype.publish = function(type, message) {
if (this.subjects[type]) {
this.subjects[type].list.map(item => {
//异步调用
setTimeout(function(){
item.fn && item.fn(type, message);
}, 0);
});
}
}
//订阅消息
pubsub.prototype.subscribe = function(type, callback) {
if (!this.subjects[type]) {
this.subjects[type] = {
count: 0,
list: []
}
}
let subId = type + '-' + this.subjects[type].count++;
this.subjects[type].list.push({
fn: callback,
id: subId
})
return subId;
}
//移除订阅,参数为订阅时返回的id
pubsub.prototype.remove = function(id) {
let arr = id.split('-');
if (arr.length > 1) {
if (this.subjects[arr[0]]) {
let len = this.subjects[arr[0]].list.length;
for (let i = 0; i < len; i++) {
if (this.subjects[arr[0]].list[i].id === id) {
this.subjects[arr[0]].list.splice(i, 1);
return true;
}
}
}
}
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
//以上是调度中心的代码
/**
* 订阅函数
* 三个参数,pubsub调度中心实例,type订阅主题,callback订阅回调函数
* */
function subscriber(pubsub, type, callback) {
if (pubsub && pubsub.subscribe) {
return pubsub.subscribe(type, callback);
}
return false;
}
/**
* 发布消息函数
* 三个参数,pubsub调度中心实例,type发布主题,message发布消息
* */
function publish(pubsub, type, message) {
if (pubsub && pubsub.publish) {
pubsub.publish(type, message);
}
}
script>
<script>
//调度中心实例
const pubsubInstance = new pubsub();
//数据全局类,存储页面要展现的变量等数据
const global = {
subscribeList: [], //订阅消息列表
publishList: []
};
//订阅按钮处理函数
function subscribe() {
let type = document.getElementById('subject').value;
if (global.subscribeList.indexOf(type) === -1) {
if (subscriber(pubsubInstance, type, handleMsg)) {
global.subscribeList.push(type);
let subscribeList = document.getElementById('subscribe_list');
subscribeList.innerHTML += `${type}`;
}
}
}
//发布按钮处理函数
function publishMsg() {
let type = document.getElementById('pub_subject').value;
let message = document.getElementById('pub_message').value;
publish(pubsubInstance, type, message);
let publishList = document.getElementById('publish_list');
publishList.innerHTML += `${type}: ${message}`;
}
//订阅消息的回调函数
function handleMsg(type, msg) {
let messageList = document.getElementById('message_list');
messageList.innerHTML += `${type}: ${msg}`;
}
script>
body>
html>
以上是发布订阅模式的实现和测试代码,代码中有注释,不难理解,就不再解释了,测试结果如下:
左侧上方是订阅的消息主题列表,订阅news和food这两个主题。
左侧下方是接收到的消息列表,接收到两条消息,主题分别是news和food。
右侧是发布的消息列表,发布了3个主题的消息,每个主题一条(分别是news,vedio,food)。
订阅者只订阅了news和food新闻,所以右侧发布了3条新闻,但是关于news和food只有两条,订阅者只接收了两条关注的新闻。
所谓命令模式就是将下要执行的业务逻辑封装到一个函数或类中,不需要具体谁来执行该命令的。在javascript中,命令模式最好的体现就是回调函数。
//ajax请求函数
function httpRequest(url, params, method, success, error) {
//具体实现省略
$.ajax({
url: url,
data: params,
method: method,
success: function(res) {
success(res);
},
error: funtion(err) {
error(err);
}
})
}
//具体调用,假设有登录请求,url为"/login",post方法
httpRequest('/login', {}, 'POST', loginSucess, loginFail);
//实现请求和具体操作的解耦,所有请求均可使用httpRequest方法,具体的success和error可单独定义。
//登录成功
function loginSuccess(data) {
//登录成功需要做的业务处理。
}
//登录失败
function loginFail(err){
//登录失败操作
}
本文就介绍到这,其它几种模式将在第二篇文章进行介绍。