12-4 JS设计模式¶
统计信息:字数 14524 阅读30分钟
简单介绍设计模式的定义和使用。设计模式需要丰富的经验后才能熟练使用。各种方式都可以实现功能,设计模式实现更优雅,更加容易阅读。
设计模式:一套反复使用,经过分类,代码设计经验的总结。
下面是主要的设计模式:
订阅发布模式(观察者模式)、单例模式、策略模式、代理模式、中介者模式、装饰器模式、外观模式、工厂模式、建造者模式、迭代器模式、享元模式、职责链模式、适配器模式、模板方法模式、备忘录模式。
订阅发布模式-观察者模式¶
原代码:多个组件互相通信,事件处理函数混杂,组件耦合高。
修改后:订阅者和发布者分离,有一个全局的对象处理事件的回调函数。
// 对象
class Event {
constructor() {
this.callbacks = {};
}
$on(name, fn) {
if (!this.callbacks[name]) {
this.callbacks[name] = [];
}
this.callbacks[name].push(fn);
}
$emit(name, arg) {
const callbacks = this.callbacks[name];
if (callbacks) {
callbacks.forEach(c => {
c.call(this, arg);
});
}
}
$off(name) {
this.callbacks[name] = null;
}
}
// 使用
let event = new Event();
event.$on('event1', arg => {
console.log(arg);
});
event.$emit('event1', 'test1');
event.$off('event1');
event.$emit('event1', 'test2');
下面是 VUE 中事件订阅的逻辑(看不懂)
export fucntion eventMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this;
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm;
}
// Vue.prototype.$once
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this;
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
const args = toArray(cbs) : cbs
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args);
} catch (e) {
handleError(e, vm);
}
}
}
return vm;
}
}
单例模式¶
保证一个类仅有一个实例,并提供全局访问点。调用这个类时,先判断是否有实例,如果不存在先创建,如果存在直接返回(保证全局只有这一个实例)
适应于:对话框(全局点击多次,也是这一个对话框)对话框创建一次即可,使用一个变量缓存即可。
function getSingle(fn) {
let result;
return function() {
return result || (result = fn.apply(this, arguemnts));
}
}
function createModalLayer() {
let div = document.createElement('div');
div.innerHTML = 'I am a modal';
div.className = 'modal';
div.style.display = 'none';
div.addEventListener('click', function() {
div.style.display = 'none';
}, false);
document.body.appendChild(div);
retunr div;
}
// 使用
createModalLayer = getSingle(createModalLayer);
document.getElementById('modal-btn').addEventListener('click', () => {
const modalLayer = createModalLayer();
modalLayer.style.display = 'block';
}, false);
策略模式¶
定义一系列的算法,每个算法封装起来,然后可以互相替换。将算法实现分离(utils函数),便于不同小算法的维护。
例如移动端计算界面滚动:那么获取当前移动端边缘的列是一个算法,计算需要显示的列是另一个算法。默认显示的列数量也是一个算法。
// 这部分可以放在配置文件json中,然后读取配置文件
const policy = {
'a': (salary) => {
return salary * 4;
},
'b': (s) => s * 3,
'c': s => s * 2,
};
// 这样写,便于代码扩展,不需要改动下面的函数
// 直接改变上面的策略对象即可
function calculate1(level, salary) {
return policy[level] ? policy[level](salary) : null;
}
表单认证,可以使用策略模式优化
let form = {};
form.submit = () => {
if (form.name.value === '' || form.password.value === '' || form.password.value.length < 5) {
console.log('error');
}
}
优化后
rules: {
name: [
{ require: true, message: 'please input' },
{ min: 3, max: 5, massage: 'please input 3 to 5 string' }
],
date: [
{ require: true }
]
};
methods: {
submit(name) {
this.$refs[name].validdate((valid) => {
if (valid) {
alert('submit');
} else {
alert('submit error');
return false;
}
});
},
reset(name) {
this.$refs[name].resetFields();
}
}
代理模式¶
通过代理的形式,访问一个对象。
原因:如果一个操作开销很大,那么设置虚拟代理的形式延迟访问对象(使用虚拟代理实现图片懒加载)
let imgFunc = (function() {
let imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
// imgNode.src = src 模拟耗时的操作
setTimeout(() => {
imgNode.src = src;
}, 1000);
}
};
})();
// 可以改成代理模式
let proxyImage = (function() {
let iimg = new Image();
img.onload = function() {
imgFunc.setSrc(this.src);
}
return {
setSrc(src) {
imgFunc.setSrc('loading.gif');
img.src = src;
}
};
})();
函数的防抖节流等
文件的延迟上传
var synchronousFile = function(id) {
// 开始同步文件 id
}
var proxyFile = (function() {
let cache = [];
let timer;
return fucntion(id) {
cache.push(id);
if (timer) {
return;
}
timer = setTimeout(() => {
synchronousFile(cache.join(','));
clearTimerout(timer);
timer = null;
cache.length = 0;
}, 2000);
}
})();
var checkbox = document.getElementByTagName('input');
for (let i = 0, c; c = checkbox[i]; i++) {
c.onclick = function() {
if (this.checked === true) {
proxyFile(this.id);
}
}
}
中介者模式¶
通过一个中介者对象,其他相关对象通过中介者来通信,而不是互相引用。当一个发生变化时,只需要通过中介者对象即可。中介者独享可以解耦。
例如在行展开过程中,不同列都会改变行数据,那我们可以统一一个行中介者,来处理每一个改动造成的数据保存(不需要每一个单独保存数据)。
装饰器模式¶
不改变自身基础上,给对象动态添加方法(常见的是高阶组件,给原始对象添加方法等)
例如在移动端界面中,可以都使用一个组件包裹移动端组件,来统一处理安卓设备的返回操作。这个可以进行优化。
包裹层的函数不能覆盖原始的函数。判断是否 Object assign 操作。
import React from 'react';
const withLog = Component => {
class NewComponent extends React.Component {
componentWillMount() {
console.log('will mount');
}
componentDidMount() {
console.log('did mount');
}
render() {
return <Component/>;
}
}
}
一个复杂的例子
export const connect = (mapStateToProps = state => state, mapDispatchToProps = {}) => (WrapComponent) => {
return class ConnectComponnet extends React.Component {
static contextTypes = {
store: PropTypes.object
};
constructor(props, context) {
super(props, context);
this.state = {
props: {}
};
}
componentDidMount() {
const { store } = this.context;
store.subscribe(() => this.update);
this.update;
}
update() {
const { store } = this.context;
const stateProps = mapStateToProps(store.getState());
const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps,
}
});
}
render() {
return <WrapComponent {...this.state.props}/>;
}
}
}
在 SSR 中,使用的 Next 函数也是类似的装饰器模式实现的
Function.prototype.before = function(fn) {
let _self = this;
// 返回包含原函数和新函数的”代理函数“
return function() {
// 执行新函数,保证this不被劫持,新函数接受的参数也会传入原函数,新函数先执行
fn.apply(this, arguments);
// 执行原函数,保证this不被劫持,并返回原函数的执行结果
let response = _self.apply(this, arguemnts);
return response;
}
};
Function.prototype.after = function(fn) {
let _self = this;
return function() {
var ret = _self.apply(this, arguments);
fn.apply(this, arguments);
return ret;
}
};
代理模式和装饰器模式基本相似,保留了对另一个对象的引用,并且向那个对象发送请求。代理模式的目的:本体访问不方便。装饰器模式:为本体动态加入行为。
外观模式¶
多个方法一起被调用(适应于兼容性代码),外部暴露一个API,内部处理各种兼容性问题。或者封装组件时(内部实现具体的逻辑,外部暴露props传参)
addEvent(dom, type, fn) {
if (dom.addEventListener) {
dom.addEventListener(type, fn, false);
}
else if (dom.attachEvent) {
dom.attachEvent('on' + type, fn);
}
else {
dom['on' + type] = fn;
}
}
stopEvent(e) {
// preventDefault/stopPropagation/returnValue/cancelBubble
// 兼容不同的浏览器,判断函数存在后再执行
}
工厂模式¶
弹窗、通知等,外部暴露一个API,然后新建一个实例
const Notification = function(options) {
if (Vue.prototype.$isServer) {
return;
}
options = options || {};
const userOnClose = options.onClose;
const id = 'notification_' + seed++;
const position = options.position || 'top-right';
options.onClose = function() {
Notifacation.close(id, userOnClose);
};
instance = new NotificationConstructor({
data: options
});
if (isVNode(options.message)) {
instance.$slots.default = [options.message];
options.message = '123';
}
instance.id = id;
instance.$mount();
document.body.appendChild(instance.$el);
instance.visible = true;
instance.dom = instance.$el;
instance.dom.style.zIndex = PopupManager.nextZindex();
}
建造者模式¶
Builder 创建相对更复杂
var Person = function(name, work) {
var _person = new Human();
_person.name = new Named(name);
_person.work = new Work(work);
return _person;
}
迭代器模式¶
数组中 forEach map 方法,不需要关注内部构造,直接可以访问每个元素(把复杂的数据结构,转换成线性的数据结构)
var each = (ary, callback) => {
for (let i = 0; i < arg.length; i++) {
callback.call(ary[i], i, ary[i]);
}
}
享元模式¶
把一系列的线性 if-else 变换成对象的 key-value 形式,便于扩展。
let Model = function(sex) {
this.sex = sex;
}
Model.prototype.takePhoto = function() {
console.log(this.sex + this.underwear);
}
let maleModel = new Model('male');
let femaleModel = new Model('female');
for (let i = 1; i <= 50; i++) {
maleModel.underwear = 'underware' + i;
maleModel.takePhoto();
}
for (let j = 1; j <= 50; j++) {
femaleModel.underwear = 'underware' + j;
femaleModel.takePhoto();
}
这里区分内部状态和外部状态。
职责链模式¶
又称为中间件机制。把请求的发起者和接受者解耦,中间的部分可以处理请求。可以沿着职责链传递请求,直到有一个对象处理。类似于中间件的SSR处理机制。
let order = (orderType, pay, stock) => {
if (orderType === 1) {
if (pay === true) {
console.log(1);
} else {
if (stock > 0) {
console.log(2);
} else {
console.log(3);
}
}
} else if (orderType = 2) {
// 其他的情况
} else {
// 其他的情况
}
}
// 这样复杂的 if-else 嵌套,造成代码层级很多,可以优化
链式处理
var order100 = (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log(1);
} else {
console.log(2);
}
}
var Chain = function(fn) {
this.fn = fn;
this.successor = null;
}
Chain.prototype.setNextSuccessor = (successor) => {
return this.successor = successor;
}
Chain.prototype.passRequest = () => {
let 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);
适配器模式¶
处理不同的接口的适配器
例如谷歌地图和百度地图的接口不同,那么我们调用不同接口,需要写一个适配器,处理不用第三方库的接口问题。
可以使用JSON传值。
模板方法模式¶
在一个方法中,定义一个骨架(模板),把具体的实现放在子类中。好处:可以不改变外部模板方法结构的情况下,重新定义算法中某些步骤的具体实现。
vue slot 和 react children
class Parent {
render () {
return (
<div>{this.props.children}</div>
);
}
}
class Stage {
render () {
return (
<Parent>
<div>this is child</div>
</Parent>
);
}
}
外部父组件实现样式渲染(固定内容),内部子组件实现内容编辑(动态编辑)
<template>
<div>
<slot />
</div>
</template>
<template>
<parent>
<div>child</div>
</parent>
</template>
备忘录模式¶
可以恢复到对象之前的某个状态(记录用户操作的数组,支持撤销和回退)
简单的备忘录直接使用数组实现
复杂的备忘录需要根据每个操作,获取反向操作并执行
总结¶
创建设计模式:工厂模式、单例模式、建造者、原型
结构化设计模式:外观、适配器、代理、装饰器、享元模式、桥接、组合
行为设计模式:策略、模板方法、观察者、迭代器、责任链、命令、备忘录、状态、访问者、终结者、解释器等