JS设计模式
文章目录
- 1 什么是设计模式?
- 2 发布-订阅模式
- 2.1 DOM事件
- 2.2 基于Broadcast Channel实现跨页面通信
- 2.3 基于localStorage实现跨页面通信
- 2.4 使用 Vue 的 EventBus 进行跨组件通信
- 2.4 使用 React 的 EventEmitter 进行跨组件通信
- 3 装饰器模式
- 3.1 React 高阶组件 HOC
- 3.2 AOP 面向切面编程
- 3.3 axios调用时添加token
- 4 单例模式
- 4.1 惰性单例
- 4.2 通用的惰性单例
- 4.3 Vuex 数据缓存
- 4.4 antd/message
- 4.5 axios取消重复请求
- 5 策略模式
- 5.1 JavaScript版本的策略模式
- 5.2 axios
- 6 迭代器模式
- 6.1 JS中的迭代器
- 6.2 根据不同浏览器选择相应的上传组件
- 7 代理模式
- 7.1 虚拟代理合并HTTP请求
- 7.2 缓存代理
- 7.3 Vue中的代理模式
1 什么是设计模式?
设计模式是针对开发中遇到的问题,提出的公认且有效的解决方案。共23种设计模式。
2 发布-订阅模式
对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知
消息的发布者将消息通过消息通道广播出去,然后订阅者通过订阅获取到想要的消息。
2.1 DOM事件
funtion func() {console.log(2)
}// 添加订阅者
document.body.addEventListener('click', func, false)// 删除订阅者
document.body.removeEventListener('click', func)
这里,我们就订阅document.body
上的click
事件,当body
被点击时,body
节点便会向订阅者发布这个消息,当然我们还可以随意的添加或者删除订阅者
2.2 基于Broadcast Channel实现跨页面通信
// A页面监听广播
// 第一步 创建实例
const bc = new BroadcastChannel('myBroadcastChannel')
// 第二部 通过onmessage设置回调事件
bc.onmessage = e => {console.log(e.data)
}// B页面发送广播
const bc = new BroadcastChannel('myBroadcastChannel')
bc.postMessage('hollow word')// 关闭广播
bc.close()
2.3 基于localStorage实现跨页面通信
// http://localhost:8080/A.html
window.addEventListener('storage', e => {// e.key 改变的key值 - msg// e.oldValue 改变前的值// e.newValue 改变后的值 - 哈哈哈if (e.key === 'msg') { ... }
})// http://localhost:8080/B.html
localStorage.setItem('msg', '哈哈哈')
2.4 使用 Vue 的 EventBus 进行跨组件通信
创建一个EventBus (本质上是Vue的一个实例对象)
import Vue from 'vux'
const EventBus = new Vue()
export default EventBus
接着在组件A和组件B中引入bus.js:import Bus from '@/utils/bus'
,组件A在 mounted
钩子中调用 Bus 的注册订阅方法 $on
传入订阅主题和回调方法,组件B中在点击事件中发布主题,让订阅该主题的组件执行回调方法。
// 组件A
mounted () {// 订阅Bus.$on('SayHollow', text => {console.log(text)})
}
// 组件B
methods: {clickEvent () {// 发布Bus.$emit('SayHollow', '啊俊俊')}
}
2.4 使用 React 的 EventEmitter 进行跨组件通信
import React, { Component } from 'react'
//一: 导入EventEmitter
import { EventEmitter } from 'events';//二: 构建事件实例
const EventBus = EventEmitter()//tips:我们可以在多个组件中去增加同一个事件的订阅,这里仅仅是示例
class Observer extends Component {componentDidMount () {// 三: 增加事件订阅this.event1 = EventBus.addListener("someEvent", (params) => {console.log(params)})}componentWillUnMount () {//四: 移除事件订阅EventBus.removeListener(this.event1)}render () {return (<div>事件监听组件</div>)}
}
class Publisher extends Component {handleClick () {const params = {}//五: 发布事件(当someEvent发布时,订阅该事件的函数就会执行)EventBus.emit('someEvent', params)}render () {return (<div><button onClick={this.handleClick.bind(this)}>发布事件</button></div>)}
}
3 装饰器模式
装饰器模式动态的给某个对象添加一些职责,并且不会影响从这个类派生的其他对象。
在传统的面向对象开发中,给对象添加功能时,我们通常会采用继承的方式,继承的方式目的是为了复用,但是随之而来也带来一些问题:
(1)父类和子类存在强耦合的关系,当父类改变时,子类也需要改变;
(2)子类需要知道父类中的细节,至少需要知道接口名,从而进行复用或复写,这样其实破坏了封装性;
(3)继承的方式,可能会创建出大量子类。比如现在有BBA三种类型的汽车,构造了一个汽车基类,三个三种类型的汽车。现在需要给汽车装上雾灯、前大灯、导航仪、刮雨器,如果采用继承的方式,那么就要构建3*4个类。但是如果把雾灯、前大灯、导航仪、刮雨器动态地添加到汽车上,那么只需要增加4个类。这种采用动态添加职责的方式就是装饰器。
装饰器的目的就是在不改变原来类的基础上,为其在运行期间动态的添加职责。
提高编程的低耦合与高可复用性
3.1 React 高阶组件 HOC
import React from 'react';const yellowHOC = WrapperComponent => {return class extends React.Component {render() {<div style={{ backgroundColor: 'yellow' }}><WrapperComponent {...this.props} /></div>;}};
};export default yellowHOC;
import React from 'react';
import yellowHOC from './yellowHOC';class TargetComponent extends Reac.Compoment {render() {return <div>66666</div>;}
}export default yellowHOC(TargetComponent);
3.2 AOP 面向切面编程
业务和系统基础功能分离,用 Decorator
很合适
log装饰器实现
export function log(target, name, decriptor) {var _origin = decriptor.value;decriptor.value = function () {console.log(`Calling ${name} with `, arguments);return _origin.apply(null, arguments);};return decriptor;
}
调用装饰器
import { log } from "./log";
class Person {@logsay(nick) {return `hi ${nick}`;}
}var person = new Person();
person.say("小明");
3.3 axios调用时添加token
4 单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点
- 避免重复的创建实例,节约不必要的开销。
- 通常会在第三方库的开发中使用到。
- 如果是项目的开发,单例模式也会用在一些数据缓存、全局通用弹窗(如登录弹窗)等一些场景中。
4.1 惰性单例
惰性单例指的是在需要的时候才创建对象实例。通常会用在全局唯一且非必需的一些场景,例如:全局弹窗、购物车列表、全局共同信息等场景。
拿书中登录弹窗的例子可以理解的更加清晰:
var createLoginLayer = (function() {var div;return function() {if(!div) {div = document.createElement('div');div.innerHtml = '登录弹窗';div.style.display = 'none';document.body.appendChild(div);}return div;}
})()document.getElementById('loginBtn').onclick = function() {var loginLayer = createLoginLayer();loginLayer.style.display = 'block';
}
在这个例子中,只有在登录按钮点击时,才会去创建登录弹窗dom节点,而不是在页面加载时就默认创建,并且,只有在第一次执行时创建登录弹窗dom节点,再次执行也不会创建多余的节点,节省了一部分性能。
4.2 通用的惰性单例
上面代码在createLoginLayer方法中,既实现了单例的逻辑,也实现了创建登录弹窗的逻辑。这违反了上节介绍的单一职责原则,如果下次仍然需要创建另一个弹窗或其他的功能,我们仍然需要将创建单例这部分逻辑再次抄一遍。这么一说相信大家也能理解到这段代码需要如何进行拆分了:将不变的部分抽离出来,也就是将要介绍的通用的惰性单例
// 通用的惰性单例
var getSingle = function(fn) {var result;return function() {return result || (resule = fn.apply(this, arguments));}
}// 创建登录弹窗的方法就可以改写成
var createLoginLayer = function() {var div = document.createElement('div');div.innerHtml = '登录弹窗';div.style.display = 'none';document.body.appendChild(div);return div;
}var createSingleLoginLayer = getSingle(createLoginLayer);document.getElementById('loginBtn').onclick = function() {var loginLayer = createSingleLoginLayer();loginLayer.style.display = 'block';
}
如上面代码,我们将创建登录弹窗和单例的逻辑分离成createLoginLayer
和getSingle
,这样之后再有了类似的需求,就可以复用到getSingle
方法实现单例。
4.3 Vuex 数据缓存
// src/store.jslet Vue;export class Store {constructor(options = {}) {if (!Vue && typeof window !== 'undefined' && window.Vue) {install(window.Vue)}}
}export function install (_Vue) {if (Vue && _Vue === Vue) {if (__DEV__) {console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.')}return}Vue = _VueapplyMixin(Vue)
}
在上面代码中,很明显可以找到一个单例模式的应用:在Store
的初始化时,只会执行一次install
方法。在install
方法中,会将Vue
赋值,并将vuex
的相关逻辑绑定到Vue实例
上。
4.4 antd/message
// components/messagelet messageInstance;function getMessageInstance(callback) {if (messageInstance) {callback(messageInstance);return;}Notification.newInstance({prefixCls,transitionName,style: { top: defaultTop }, // 覆盖原来的样式getContainer,maxCount,},instance => {if (messageInstance) {callback(messageInstance);return;}messageInstance = instance;callback(instance);},);
}
在上面代码中,我们可以看到当已经存在messageInstance
时,会直接复用对应的实例:callback(messageInstance)
,否则的话,将会赋值messageInstance = instance
。
先看下面这段代码:
<template><div><a-button @click="handleMessage">message</a-button></div>
</template>export default {methods: {handleMessage() {this.$message.info('handleMessage');},},created() {this.$message.config({top: '100px',duration: 1,});},
};
这是一个vue组件,在created
时,我们配置了message
的参数,当点击按钮时,会执行方法调用message.info
方法弹出组件。在message
源码中,会通过执行notice
方法弹出弹窗,而如果我们是第一次弹出弹窗时:
会创建一个ant-message
节点其中top: 100px就是刚才设置的配置项,当我们再次触发时:
可以看到会复用刚刚创建的dom节点,并在内部创建一个弹窗。那么这样做有什么好处呢?
- 最显然的一个优势就是复用了dom节点
- 还有一个优势,当我们连续多次点击时,可以看到弹窗的效果是按顺序依次显示的,只有一个dom节点可以保证多次弹出的弹窗只有一个父节点,那么弹窗位置只要由父节点控制即可,不需要每次都重新计算,效果如图所示:
4.5 axios取消重复请求
import axios from "axios";
const CancelToken = axios.CancelToken;
let cancelId = 0;
let cancelArray = [];// 添加请求拦截器
axios.interceptors.request.use(function (config) {// 在请求时,可以添加自己的特点标识去筛选出需要重复取消的接口const source = CancelToken.source();cancelId++;const id = cancelId;config.cancelId = id;config.cancelToken = source.token;const cancelIndex = cancelArray.findIndex(e => e.url === config.url);cancelArray.push({id,url: config.url,source})if (cancelIndex > -1) {cancelArray[cancelIndex].source.cancel('取消重复请求');cancelArray.splice(cancelIndex, 1)}return config;
}, function (error) {return Promise.reject(error);
});// 添加响应拦截器
axios.interceptors.response.use(function (response) {const cancelIndex = cancelArray.findIndex(e => e.id === response.cancelId);if (cancelIndex >= -1) {cancelArray.splice(cancelIndex, 1)}// 对响应数据做点什么return response;
}, function (error) {if (axios.isCancel(error)) {// 如果是取消的接口,可以自行返回一个特定标识console.log('isCancel')} else {// 对响应错误做点什么return Promise.reject(error);}
});export default axios
5 策略模式
定义一系列算法,把他们一个个封装起来,并且使他们可以相互替换。
在平时的工作中也存在非常多应用场景,比如业务中经常会存在针对不同场景执行不同逻辑的情况,就可以考虑使用策略模式
5.1 JavaScript版本的策略模式
业务描述:实现一个计算年终奖的功能,绩效为S的人有4倍工资,A为3倍工资,B为2倍工资。
var strategies = {'S': function(salary) {return salary * 4;},'A': function(salary) {return salary * 3;},'B': function() {return salary * 2;}
}var calculateBouns = function(level, salary) {return strategies[level](salary);
}calculateBouns('A', 2000);
5.2 axios
axios
既可用于浏览器中又可用于node
环境中,但通过源码可以得知:在不同的环境,将使用不同的方式发起请求
// lib/defaults.jsfunction getDefaultAdapter() {var adapter;if (typeof XMLHttpRequest !== 'undefined') {// For browsers use XHR adapteradapter = require('./adapters/xhr');} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {// For node use HTTP adapteradapter = require('./adapters/http');}return adapter;
}
通过这段代码,可以看到在axios
中,我们会根据环境赋值不同的adapter
,但XHR
和Http
发送请求的方式并不相同,那么如何保证在不同场景使用方式相同呢?其实,axios
会将不同的逻辑在各自内部处理,最终暴露出相同的调用方式,简单看下以下两部分代码:
// lib/adapters/xhr.jsmodule.exports = function xhrAdapter(config) {return new Promise((resolve, reject) => {/**省略xxxx代码*/var request = new XMLHttpRequest();/**省略xxxx代码*/request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);})request.onreadystatechange = function handleLoad() {/**省略xxxx代码*/// 对response进行校验,满足条件则请求成功 resolve(response)settle(resolve, reject, response);}
}// lib/adapters/http.jsmodule.exports = function httpAdapter(config) {return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {var resolve = function resolve(value) {resolvePromise(value);};var reject = function reject(value) {rejectPromise(value);};/**省略xxxx代码*/var transport;// 源码中有不同逻辑的判断,这里简化为其中一种情况transport = isHttpsProxy ? https : http;/**省略xxxx代码*/var req = transport.request(options, function handleResponse(res) {/**省略xxxx代码*/res.on('end', function handleStreamEnd() {/**省略xxxx代码*/settle(resolve, reject, response);});})})
}
两个都返回Promise
,在不同的方法中,各自处理了响应逻辑。在于使用时,也就不需要再区分不同的环境了。
6 迭代器模式
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序返回其中的每个元素。
6.1 JS中的迭代器
在JavaScript中,相信大家经常听到过迭代、循环之类的名词,把这两个概念区分一下:
- 循环,循环就是在满足一定条件时,重复执行同一段代码,典型的例子:
do...while
- 迭代,迭代是指按顺序逐个访问对象中的每一项,典型的例子:
forEach
那么什么样的对象可以被迭代呢?需要满足什么条件呢?
- 要成为可迭代对象,对象必须要实现必须实现
@@iterator
方法,通常可以访问常量Symbol.iterator
访问该属性。 - 目前的内置可迭代对象有:
String
、Array
、TypedArray
、Map
、Set
,他们的原型对象都实现了@@iterator
方法。
当这个对象是可迭代对象时,我们可以通过调用[Symbol.iterator]
方法来按顺序遍历对象中的每一项:
const arr = [1, 2, 3, 4];
// 迭代器
const iterator = arr[Symbol.iterator]()
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
遍历比如forEach
、for...of
等方法,其实就是封装了一个遍历可迭代对象的方法,属于内部迭代器。而当我们通过调用next
方法自行控制迭代对象遍历时,比如ES6中的生成器函数,这种就属于外部迭代器。
6.2 根据不同浏览器选择相应的上传组件
提供一个可以被迭代的方法,使得getActiveUploadObj
、getFlashUploadObj
、getFormUploadObj
依照优先级被迭代
我们会优先选择控件上传,如果没有安装上传控件则使用Flash上传,如果Flash也没有安装,那就只好使用浏览器原生的表单上传了
// 定义各个上传方法
var getActiveUploadObj = function() {try {return new ActiveXObject('TXFTNActiveX.FTNUpload');} cache(e) {return false}
}var getFlashUploadObj = function() {if (supportFlash()) {var str = '<object type="application/x-shockwave-flash"></object>'return $(str).appendTo($('body'));}return false
}var getFormUploadObj = function() {var str = '<input name="file" type="file" />' // 表单上传return $(str).appendTo($('body'));
}// 按优先级迭代函数
var iteratorUploadObj = function() {for (var i = 0, fn; fn = arguments[i++];) {var uploadObj = fn();if (uploadObj !== false) {return uploadObj}}
}// 获取可上传upload对象
var uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)
各个上传对象的方法互不干扰,可以很好的维护和扩展代码。
7 代理模式
为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问。客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
保护代理和虚拟代理:
- 代理B可以帮助A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B处被拒绝掉,这种代理叫做保护代理
- 假设现实中花的价格不菲,导致在程序世界里,new Flower也是一个代价昂贵的操作,那么我们可以把new Flower的操作交给代理B去执行,代理B会选择在A心情好时在执行new Flower,这种代理叫做虚拟代理
- 虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。保护代理用于控制不同权限的对象对目标对象的访问。保护代理用于控制不同权限的对象对目标对象的访问,而虚拟代理是最常用的一种代理模式。
7.1 虚拟代理合并HTTP请求
假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外一台服务器上面:
var synchronousFile = function (id) {console.log('开始同步文件', id)
}var proxySynchronousFile = function() {var cache = [],timer;return function(id) {cache.push(id);if (timer) {return;}timer = settimeout(() => {synchronousFile(cache.join(','));clearTimeout(timer);timer = null;cache.length = 0; // 清空id集合}, 2000)}
}()var checkbox = document.getElementsByTagName('input')for (var i = 0, c; c = checkbox[i++]) {c.onclick = function() {if (this.checked === true) {proxySynchronousFile(this.id);}}
}
通过一个代理函数proxySynchronousFile
来收集一段时间之内的请求,最后一次性发给服务器,如果不是实时性要求很高的系统,有一点延迟并不会带来太大的副作用,却能大大减轻服务器的压力
7.2 缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面的存储的运算结果。
var proxyMult = (function() {var cache = {};return function() {var args = Array.prototype.join.call(arguments, ',');if (args in cache) {return cache[args];}return cache[args] = mult.apply(this, arguments);}
})()proxyMult(1, 2, 3, 4) // 24
proxyMult(1, 2, 3, 4) // 24
当第二次调用proxyMult(1, 2, 3, 4)
时,本体mult
函数并没有被计算,proxyMult
直接返回了之前计算好的结果。通过增加缓存代理的方式,mult
函数可以继续专注于自身的职责——计算乘积,缓存功能是由代理对象实现的。
7.3 Vue中的代理模式
当我们使用组件中的data、props和methods时,只需要调用this.xxx即可,拿initData的部分看下源码中是怎么处理的:
// src/core/instance/state.jsfunction initData (vm: Component) {let data = vm.$options.datadata = vm._data = typeof data === 'function'? getData(data, vm): data || {}if (!isPlainObject(data)) {data = {}process.env.NODE_ENV !== 'production' && warn('data functions should return an object:\n' +'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// proxy data on instanceconst keys = Object.keys(data)const props = vm.$options.propsconst methods = vm.$options.methodslet i = keys.lengthwhile (i--) {const key = keys[i]if (process.env.NODE_ENV !== 'production') {if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm)}}if (props && hasOwn(props, key)) {process.env.NODE_ENV !== 'production' && warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm)} else if (!isReserved(key)) {proxy(vm, `_data`, key)}}// observe dataobserve(data, true /* asRootData */)
}
可以看到首先我们设置了vm._data
,后面又执行了 proxy(vm, _data, key)
将vm._data.xxx
代理到vm.xxx
上,最后通过observe(data, true)
监听data的变化,将data变为是响应式的。
所以,要知道为什么可以直接使用this.xxx调用到组件中的data,只需要了解proxy的实现即可:
const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
}export function proxy (target: Object, sourceKey: string, key: string) {sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]}sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val}Object.defineProperty(target, key, sharedPropertyDefinition)
}
可以看到,通过修改get
和 set
方法后,当我们获取vm.xxx
时,实际则会取到this[sourceKey][key]
,也就是vm._data.xxx
。