一、单例模式
1. 什么是单例模式
单例模式的定义是,保障一个类仅有一个实例,并提供一个拜访它的全局拜访点。
有一些对象,比方线程池/全局缓存/浏览器中的 window
对象等等,咱们就只须要一个实例。
上面将依据理论场景进行介绍。
2. 理论场景
1. 登录浮窗
当咱们单击登录按钮时,页面中会呈现一个登录的浮窗,而这个登录浮窗是惟一的,无论单击多少次登录按钮,这个浮窗都只会被创立一次,那么这个登录浮窗就适宜用单例模式来创立。
1.1 传统做法
传统做法在页面加载实现时,就创立好登录浮窗,当用户点击登录按钮时,显示登录浮窗,实现代码如下:
<button id="loginBtn">登录</button>
var loginLayer = (() => { let div = document.createElement('div') div.innerHTML = '我是登录弹窗' div.style.display = 'none' document.body.appendChild(div) return div})()document.getElementById('loginBtn').onclick = () => { loginLayer.style.display = 'block'}
上述代码有以下毛病:
- 在无需登录的状况下,也会新增登录浮窗的
DOM
节点,节约性能。
当初优化一下,将代码改为,在用户点击登录按钮后,才新增登录浮窗的 DOM
节点。
代码如下:
var createLoginLayer = () => { let div = document.createElement('div') div.innerHTML = '我是登录弹窗' div.style.display = 'none' document.body.appendChild(div) return div}document.getElementById('loginBtn').onclick = () => { const loginLayer = createLoginLayer() loginLayer.style.display = 'block'}
上述代码也存在缺点,具体如下:
- 每次点击登录按钮,都会创立一个登录浮窗,频繁的创立
DOM
节点更加节约性能。
实际上,咱们只须要创立一次登录浮窗。
1.2 单例模式
通过单例模式,重构上述代码。
const createLoginLayer = () => { const div = document.createElement('div') div.innerHTML = '我是登录弹窗' div.style.display = 'none' console.log(123) document.body.appendChild(div) return div}const createSingle = (function () { var instance = {} return function (fn) { if (!instance[fn.name]) { instance[fn.name] = fn.apply(this, arguments) } return instance[fn.name] }})()const createIframe = function () { const iframe = document.createElement('iframe') document.body.appendChild(iframe) iframe.style.display = 'none' return iframe}const createSingleLoginLayer = createSingle(createLoginLayer)const createSingleIframe = createSingle(createIframe)document.getElementById('loginBtn').onclick = () => { const loginLayer = createSingleLoginLayer const iframe = createSingleIframe loginLayer.style.display = 'block' iframe.style.display = 'block'}
通过重构,代码做了以下优化:
- 将创立实例对象
createLoginLayer
/createIframe
的职责和治理单例对象createSingle
的职责拆散,合乎繁多职责准则; - 通过闭包存储实例,并进行判断,不论点击登录按钮多少次,只创立一个登录浮窗实例;
- 易于扩大,当下次须要创立页面中惟一的
iframe
/script
等其余标签时,能够间接复用该逻辑。
3. 总结
单例模式是一种简略但十分实用的模式,特地是惰性单例技术,在适合的时候才创建对象,并且只创立惟一的一个。更微妙的是,创建对象和治理单例的职责被散布在两个不同的办法中,这两个办法组合起来才具备单例模式的威力。
二、策略模式
1. 什么是策略模式
当咱们打算国庆进来玩耍时,在交通形式上,咱们能够抉择贵而快的飞机、价格中等但稍慢的动车、便宜但超级慢的火车,依据不同的人,抉择对应的交通形式,且能够随便更换交通形式,这就是策略模式。
策略模式的定义是,定义一系列算法,把它们一个个封装起来,并且使它们能够互相替换。
2. 理论场景
1. 计算年终奖
1.1 传统做法
有一个计算员工年终奖的需要,假如,绩效为 S
的员工年终奖是 4
倍工资,绩效为 A
的员工年终奖是 3
倍工资,绩效为 B
的员工年终奖是 2
倍工资,上面咱们来计算员工的年终奖。
var calculateBonus = function(performanceLevel, salary) { if (performanceLevel === 'S') { return salary * 4; } if (performanceLevel === 'A') { return salary * 3; } if (performanceLevel === 'B') { return salary * 2; }};calculateBonus('B', 20000); // 输入:40000 calculateBonus( 'S', 6000 ); // 输入:24000
上述代码有以下毛病:
- 应用
if-else
语句形容逻辑,代码宏大; - 不足弹性,如果须要批改绩效
S
的奖金系数,必须批改calculateBonus
函数,违反了凋谢-关闭准则; - 无奈再次复用,当其余中央须要用到这套逻辑,只能再复制一份。
1.2 策略模式做法
应用策略模式改进后
const strategies = { S: salary => { return salary * 4 }, A: salary => { return salary * 3 }, B: salary => { return salary * 2 }}const calculateBonus = (level, salary) => { return strtegies[level](salary)}console.log(calculateBonus('s', 20000))console.log(calculateBonus('a', 10000))
能够看到上述代码做了以下改变:
- 策略类
strategies
封装了具体的算法和计算过程(每种绩效的计算规定); - 环境类
calculateBonus
承受申请,把申请委托给策略类strategies
(员工的绩效和工资; - 将算法的应用和算法的实现拆散,代码清晰,职责明显;
- 打消大量的
if-else
语句。
1.3 小结
策略模式使代码可读性更高,易于拓展更多的策略算法。当绩效系数扭转,或者绩效等级减少,咱们只须要为 strategies
调整或新增算法,合乎凋谢-关闭准则。
2. 表单校验
当网页上的表单须要校验输入框/复选框等等规定时,如何去实现呢?
当初有一个注册用户的表单需要,在提交表单之前,须要验证以下规定:
- 用户名不能为空
- 明码长度不能少于 6 位
- 手机号码必须合乎格局
2.1 传统做法
应用 if-else
语句判断表单输出是否合乎对应规定,如不合乎,提醒谬误起因。
<!DOCTYPE html><html><head> <title></title></head><body> <form id='registerForm' action="xxx" method="post"> 用户名:<input type="text" name="userName"> 明码:<input type="text" name="password"> 手机号:<input type="text" name="phone"> <button>提交</button> </form> <script type="text/javascript"> let registerForm = document.getElementById('registerForm') registerForm.onsubmit = () => { if (registerForm.userName.value) { alert('用户名不能为空') return false } if (registerForm.password.value.length < 6) { alert('明码长度不能少于6') return false } if (!/(^1[3|5|8][0-9]$)/.test(registerForm.phone.value)) { alert('手机号码格局不正确') return false } } </script></body></html>
上述代码有以下毛病:
onsubmit
函数宏大,蕴含大量if-else
语句;onsubmit
不足弹性,当有规定须要调整,或者须要新增规定时,须要改变onsubmit
函数外部,违反凋谢-关闭准则;- 算法复用性差,只能通过复制,复用到其余表单。
2.2 策略模式做法
应用策略模式重构上述代码。
<!DOCTYPE html><html><head> <title></title></head><body> <form action="http://xxx.com/register" id="registerForm" method="post"> 请输出用户名: <input type="text" name="userName" /> 请输出明码: <input type="text" name="password" /> 请输出手机号码: <input type="text" name="phoneNumber" /> <button> 提交 </button> </form> <script type="text/javascript" src="index.js"> </script> </body> </html>
// 表单domconst registerForm = document.getElementById('registerForm')// 表单规定const rules = { userName: [ { strategy: 'isNonEmpty', errorMsg: '用户名不能为空' }, { strategy: 'minLength:10', errorMsg: '用户名长度不能小于10位' } ], password: [ { strategy: 'minLength:6', errorMsg: '明码长度不能小于6位' } ], phoneNumber: [ { strategy: 'isMobile', errorMsg: '手机号码格局不正确' } ]}// 策略类var strategies = { isNonEmpty: function(value, errorMsg) { if (value === '') { return errorMsg; } }, minLength: function(value, errorMsg, length) { console.log(length) if (value.length < length) { return errorMsg; } }, isMobile: function(value, errorMsg) { if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) { return errorMsg; } }};// 验证类const Validator = function () { this.cache = []}// 增加验证办法Validator.prototype.add = function ({ dom, rules}) { rules.forEach(rule => { const { strategy, errorMsg } = rule console.log(rule) const [ strategyName, strategyCondition ] = strategy.split(':') console.log(strategyName) const { value } = dom this.cache.push(strategies[strategyName].bind(dom, value, errorMsg, strategyCondition)) })}// 开始验证Validator.prototype.start = function () { let errorMsg this.cache.some(cacheItem => { const _errorMsg = cacheItem() if (_errorMsg) { errorMsg = _errorMsg return true } else { return false } }) return errorMsg}// 验证函数const validatorFn = () => { const validator = new Validator() console.log(validator.add) Object.keys(rules).forEach(key => { console.log(2222222, rules[key]) validator.add({ dom: registerForm[key], rules: rules[key] }) }) const errorMsg = validator.start() return errorMsg}// 表单提交registerForm.onsubmit = () => { const errorMsg = validatorFn() if (errorMsg) { alert(errorMsg) return false } return false}
上述代码通过 strategies
定义规定算法,通过 Validator
定义验证算法,将规定和算法拆散,咱们仅仅通过配置的形式就能够实现表单的校验,这些校验规定也能够复用在程序的任何中央,还能作为插件的模式,不便的被移植到其余我的项目中。
3. 总结
策略模式是一种罕用且无效的设计模式,通过上述例子,能够总结出策略模式的一些长处:
- 策略模式利用组合/委托和多态等技术和思维,能够无效的防止多重条件抉择语句;
- 策略模式提供了对凋谢-关闭准则的完满反对,将算法封装中独立的策略类中,使得它们易于切换/了解/扩大;
- 在策略模式中利用组合和委托来让
Context
领有执行算法的能力,这也是继承的一种更轻便的代替计划。
三、代理模式
1. 什么是代理模式
代理模式是为一个对象提供一个代用品或占位符,以便管制对它的拜访。
代理模式的要害是,当客户不不便间接拜访一个对象或者不满足需要的时候,提供一个替身对象来管制对这个对象的拜访,客户实际上拜访的是替身对象。
2. 模仿场景
1. 小明送花给小白
1.1 传统做法
传统做法是小明间接把花送给小白,小白接管到花,代码如下:
const Flower = function () { return '玫瑰'}const xiaoming = { sendFlower: target => { const flower = new Flower() target.receiveFlower(flower) }}const xiaobai = { receiveFlower: flower => { console.log('收到花', flower) }}xiaoming.sendFlower(xiaobai)
1.2 代理模式
然而,小明并不意识小白,他想要通过小代,帮他打探小白的状况,在小白情绪好的时候送花,这样成功率更高。代码如下:
const Flower = function () { return '玫瑰'}const xiaoming = { sendFlower: target => { const flower = new Flower() target.receiveFlower(flower) }}const xiaodai = { receiveFlower: flower => { xiaobai.listenGoodMood().then(() => { xiaobai.receiveFlower(flower) }) }}const xiaobai = { receiveFlower: flower => { console.log('收到花', flower) }, listenGoodMood: fn => { return new Promise((reslove, reject) => { // 10秒后,情绪变好 reslove() }) }}xiaoming.sendFlower(xiaodai)
以上,小明通过小代,监听到小白情绪的情绪变动,抉择在小白情绪好时送花给小白。不仅如此,小代还能够做以下事件:
- 帮忙小白过滤掉一些送花的申请,这就叫做爱护代理;
- 帮忙小明,在小白情绪好时,再执行买花操作,这就叫做虚构代理。虚构代理把一些开销很大的对象,提早到真正须要它的时候才去创立。
3. 理论场景
1. 图片预加载
图片预加载时一种常见的技术,如果间接给 img 标签节点设置 src 属性,因为图片过大或网络不佳,图片的地位往往有一段时间时空白。
1.1 传统做法
const myImage = (() => { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: src => { imgNode.src = src } }})()myImage.setSrc('https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png')
通过开发者工具把网速设置为 5kb/s 时,会发现在很长一段时间内,图片地位是空白的。
1.2 虚构代理
上面用虚构代理优化该性能,把加载图片的操作交给代理函数实现,在图片加载时,先用一张loading 图占位,当图片加载胜利后,再把它填充进 img 节点。
代码如下:
const myImage = (() => { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: src => { imgNode.src = src } }})()const loadingSrc = '../../../../img/loading.gif'const imgSrc = 'https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png'const proxyImage = (function () { const img = new Image() img.onload = () => { myImage.setSrc(img.src) } return { setSrc: src => { myImage.setSrc(loadingSrc) img.src = src } }})()proxyImage.setSrc(imgSrc)
上述代码有以下长处:
- 通过
proxyImage
管制了对MyImage
的拜访,在MyImage
未加载胜利之前,应用loading
图占位; - 践行繁多职责准则,给
img
节点设置src
的函数MyImage
,预加载图片的函数proxyImage
,都只有一个职责; - 践行凋谢-关闭准则,给
img
节点设置src
和预加载图片的性能,被隔离在两个对象里,它们能够各自变动不影响对方。
2. 合并HTTP申请
假如咱们要实现一个同步文件的性能,通过复选框,当复选框选中的时候,将该复选框对应的 id 传给服务器,通知服务器须要同步 id 对应的文件。
思考一下,会发现,如果每选中一个复选框,就申请一次接口,假如 1s 内选中了 10 个复选框,那么就要发送 10 次申请。
2.1 虚构代理
能够通过虚构代理来优化上述做法,新增一个代理,帮忙复选框发动同步文件的申请,收集在这 1s 内的申请,1s 后再一起把这些文件 id 发送到服务器。
代码如下:
<!DOCTYPE html><html><meta charset="utf-8" /><head> <title></title></head><body> a <input type="checkbox" value="a" /> b <input type="checkbox" value="b" /> c <input type="checkbox" value="c" /> d <input type="checkbox" value="d" /> <script type="text/javascript" src="index.js"> </script></body> </html>
const synchronousFile = cache => { console.log('开始同步文件,id为:'+ cache.join('/'))}const proxySynchronousFile = (() => { const cache = [] let timer return id => { console.log(id) cache.push(id) if (timer) { return } timer = setTimeout(() => { synchronousFile(cache) clearTimeout(timer) timer = null cache.length = 0 }, 2000) }})()const checkbox = document.getElementsByTagName('input')Array.from(checkbox).forEach(i => { console.log(i) i.onclick = () => { if (i.checked) { proxySynchronousFile(i.value) } }})
3. ajax异步申请数据
在列表须要分页时,同一页的数据实践上只须要去后盾拉取一次,能够把这些拉取过的数据缓存下来,下次申请时间接应用缓存数据。
3.1 缓存代理
应用缓存代理实现上述性能,代码如下:
(async function () { function getArticle (currentPage, pageSize) { console.log('getArticle', currentPage, pageSize) // 模仿一个ajax申请 return new Promise((resolve, reject) => { resolve({ ok: true, data: { list: [], total: 10, params: { currentPage, pageSize } } }) }) } const proxyGetArticle = (() => { const caches = [] return async (currentPage, pageSize) => { const cache = Array.prototype.join.call([currentPage, pageSize],',') if (cache in caches) { return caches[cache] } const { data, ok } = await getArticle(currentPage, pageSize) if (ok) { caches[cache] = data } return caches[cache] } })() // 搜寻第一页 await proxyGetArticle(1, 10) // 搜寻第二页 await proxyGetArticle(2, 10) // 再次搜寻第一页 await proxyGetArticle(1, 10) })()
通过缓存代理,在第二次申请第一页的数据时,间接在缓存数据中拉取,毋庸再次从服务器申请数据。
4. 总结
下面依据理论场景介绍了虚构代理和缓存代理的做法。
当咱们不不便间接拜访某个对象时,找一个代理办法帮咱们去拜访该对象,这就是代理模式。
可通过 github源码 进行实操练习。
心愿本文能对你有所帮忙,感激浏览❤️~
欢送关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。