# 设计模式

# 一、订阅/发布模式

pub/sub 这个应该⼤家⽤到最⼴的设计模式了,在这种模式中,并不是⼀个对象调⽤另⼀个对象的⽅法,⽽是⼀个对象订阅另⼀个对象的特定活动并在状态改变后获得通知。订阅者因此也成为观察者,⽽被观察的对象成为发布者或者主题。当发⽣了⼀个重要事件时候发布者会通知(调⽤)所有订阅者并且可能经常已事件对象的形式传递消息。

class Event {
    constructor() {
        this.callbacks = {}
    }
    $off(name) {
        this.callbacks[name] = null
    }
    $emit(name, args) {
        let cbs = this.callbacks[name]
        if (cbs) {
            cbs.forEach(c => {
                c.call(this, args)
            })
        }
    }
    $on(name, fn) {
        (this.callbacks[name] || (this.callbacks[name] = [])).push(fn)
    }
}
let event = new Event()
event.$on('event1', function (arg) {
    console.log('事件1', arg)
})
event.$on('event1', function (arg) {
    console.log('⼜⼀个事件1', arg)
})
event.$on('event2', function (arg) {
    console.log('事件2', arg)
})
event.$emit('event1', { name: 'js' })
event.$emit('event2', { name: '网络' })
event.$off('event1')
event.$emit('event1', { name: '设计模式' })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 二、单例模式

单例模式的定义:保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。实现的⽅法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了⼀个类只有⼀个实例对象。

适⽤场景:⼀个单⼀对象。⽐如:弹窗,⽆论点击多少次,弹窗只应该被创建⼀次,实现起来也很简单,⽤⼀个变量缓存即可。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .model {
            border: 1px solid black;
            position: fixed;
            width: 300px;
            height: 300px;
            top: 20%;
            left: 50%;
            margin-left: -150px;
            text-align: center;
        }
    </style>
</head>
<body>
    <div id="loginBtn">点我</div>
    <script>
        var getSingle = function (fn) {
            var result;
            return function () {
                return result || (result = fn.apply(this, arguments));
            }
        };
        var createLoginLayer = function () {
            var div = document.createElement('div');
            div.innerHTML = '我是登录浮窗';
            div.className = 'model'
            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';
        };
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# 三、策略模式

策略模式的定义:定义⼀系列的算法,把他们⼀个个封装起来,并且使他们可以相互替换。策略模式的⽬的就是将算法的实现分离开来。

⼀个基于策略模式的程序⾄少由两部分组成。第⼀个部分是⼀组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。第⼆个部分是环境类Context(不变),Context接受客户的请求,随后将请求委托给某⼀个策略类。要做到这⼀点,说明Context中要维持对某个策略对象的引⽤。

# 举个栗⼦

奖⾦计算,绩效为 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用策略

// 使用策略
var strategies = {
    "S": function (salary) {
        return salary * 4;
    },
    "A": function (salary) {
        return salary * 3;
    },
    "B": function (salary) {
        return salary * 2;
    }
};
var calculateBonus = function (level, salary) {
    if (level in strategies) {
        return strategies[level](salary);
    }
    return salary;
};
console.log(calculateBonus('S', 20000));// 输出:80000
console.log(calculateBonus('A', 10000));// 输出:30000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 表单校验

不使用策略

// 正常写法
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
    if (registerForm.userName.value === '') {
        alert('⽤户名不能为空');
        return false;
    }
    if (registerForm.password.value.length < 6) {
        alert('密码⻓度不能少于 6 位');
        return false;
    }
    if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
        alert('⼿机号码格式不正确');
        return false;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

使用策略

var strategies = {
    isNonEmpty: function (value, errorMsg) {
        if (value === '') {
            return errorMsg;
        }
    },
    minLength: function (value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg;
        }
    },
    isMobile: function (value, errorMsg) { // ⼿机号码格式
        if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    }
};
var Validator = function () {
    this.cache = []; // 保存校验规则
};
Validator.prototype.add = function () {
    var ary = rule.split(':');
    this.cache.push(function () { //
        var strategy = ary.shift();
        ary.unshift(dom.value);
        ary.push(errorMsg); //
        return strategies[strategy].apply(dom, ary);
    });
};
Validator.prototype.start = function () {
    for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
        var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
        if (msg) { // 如果有确切的返回值,说明校验没有通过
            return msg;
        }
    }
};
var validataFunc = function () {
    var validator = new Validator(); // 创建⼀个 validator 对象
    /***************添加⼀些校验规则****************/
    validator.add(registerForm.userName, 'isNonEmpty', '⽤户名不能为空');
    validator.add(registerForm.password, 'minLength:6', '密码⻓度不能少于 6位');
    validator.add(registerForm.phoneNumber, 'isMobile', '⼿机号码格式不正确');
    var errorMsg = validator.start(); // 获得校验结果
    return errorMsg; // 返回校验结果
}
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
    var errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
    if (errorMsg) {
        alert(errorMsg);
        return false; // 阻⽌表单提交
    }
};   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 四、代理模式

代理模式的定义:为⼀个对象提供⼀个代⽤品或占位符,以便控制对它的访问。

常⽤的虚拟代理形式:某⼀个花销很⼤的操作,可以通过虚拟代理的⽅式延迟到这种需要它的时候才去创建(例:使⽤虚拟代理实现图⽚懒加载)。

图⽚懒加载的⽅式:先通过⼀张loading图占位,然后通过异步的⽅式加载图⽚,等图⽚加载好了再把完成的图⽚加载到img标签⾥⾯。

var imgFunc = (function () {
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function (src) {
            imgNode.src = src;
        }
    }
})();
var proxyImage = (function () {
    var img = new Image();
    img.onload = function () {
        imgFunc.setSrc(this.src);
    }
    return {
        setSrc: function (src) {
            imgFunc.setSrc('./loading,gif');
            img.src = src;
        }
    }
})();
proxyImage.setSrc('./pic.png');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

假设我们在做⼀个⽂件同步的功能,当我们选中⼀个checkbox的时候,它对应的⽂件就会被同步到另外⼀台备⽤服务器上⾯。当⼀次选中过多时,会产⽣频繁的⽹络请求。将带来很⼤的开销。可以通过⼀个代理函数proxySynchronousFile来收集⼀段时间之内的请求, 最后⼀次性发送给服务器.

函数的节流防抖,也是一种代理模式。

# 五、中介者模式

中介者模式的定义:通过⼀个中介者对象,其他所有的相关对象都通过该中介者对象来通信,⽽不是相互引⽤,当其中的⼀个对象发⽣改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。

例如:现实⽣活中,航线上的⻜机只需要和机场的塔台通信就能确定航线和⻜⾏状态,⽽不需要和所有⻜机通信。同时塔台作为中介者,知道每架⻜机的⻜⾏状态,所以可以安排所有⻜机的起降和航线安排。

中介者模式适⽤的场景:例如购物⻋需求,存在商品选择表单、颜⾊选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

redux,vuex 都属于中介者模式的实际应⽤,我们把共享的数据,抽离成⼀个单独的store, 每个都通过store这个中介来操作对象⽬的就是减少耦合。

# 六、装饰器模式

装饰者模式的定义:在不改变对象⾃身的基础上,在程序运⾏期间给对象动态地添加⽅法。常⻅应⽤,react的⾼阶组件, 或者react-redux中的@connect 或者⾃⼰定义⼀些⾼阶组件。

import React from 'react'

// 打印日志的功能
const withLog = Component => {
    // 类组件
    class NewComponent extends React.Component {
        componentWillMount() {
            console.time(`CompoentRender`)
            console.log(`准备完毕了`)
        }
        render() {
            return <Component {...this.props}></Component>
        }
        componentDidMount() {
            console.timeEnd(`CompoentRender`)
            console.log(`渲染完毕了`)
        }
    }
    return NewComponent
}
export { withLog }

@withLog
class XX {
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 登陆案例

⽐如⻚⾯中有⼀个登录button,点击这个button会弹出登录浮层,与此同时要进⾏数据上报, 来统计有多少⽤户点击了这个登录 button。

不使用装饰器

var showLogin = function(){
 console.log( '打开登录浮层' );
 log( this.getAttribute( 'tag' ) );
}
var log = function( tag ){
 console.log( '上报标签为: ' + tag );
 (new Image).src = 'http:// xxx.com/report?tag=' + tag;
}
document.getElementById( 'button' ).onclick = showLogin;
1
2
3
4
5
6
7
8
9

使用装饰器

// 定义一个前置函数
Function.prototype.before = function (beforefn) {
    var __self = this; // 保存原函数的引⽤
    return function () { // 返回包含了原函数和新函数的"代理"函数
        beforefn.apply(this, arguments); // 执⾏新函数,且保证 this 不被劫持,新函数接受的参数也会被原封不动地传⼊原函数,新函数在原函数之前执⾏
        return __self.apply(this, arguments); // 执⾏原函数并返回原函数的执⾏结果,并且保证 this 不被劫持
    }
}
//定义一个后置函数
Function.prototype.after = function (afterfn) {
    var __self = this;
    return function () {
        var ret = __self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
};

var showLogin = function () {
    console.log('打开登录浮层');
}
var log = function () {
    console.log('上报标签为: ' + this.getAttribute('tag'));
}
showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

装饰者模式和代理模式的结构看起来⾮常相像,这两种模式都描述了怎样为对象提供 ⼀定程度上的间接引⽤,它们的实现部分都保留了对另外⼀个对象的引⽤,并且向那个对象发送 请求。代理模式和装饰者模式最重要的区别在于它们的意图和设计⽬的。代理模式的⽬的是,当直接访问本体不⽅便或者不符合需要时,为这个本体提供⼀个替代者。本体定义了关键功能,⽽代理提供或拒绝对它的访问,或者在访问本体之前做⼀些额外的事情。装饰者模式的作⽤就是为对象动态加⼊⾏为。

其实Vue中的v-input,v-checkbox也可以认为是装饰器模式,对原⽣的input和checkbox做⼀层装饰。

# 七、外观模式

外观模式即让多个⽅法⼀起被调⽤涉及到兼容性,参数⽀持多格式,有很多这种代码,对外暴露统⼀的api,⽐如上⾯的$on⽀持数组,$off参数⽀持多个情况, 对⾯只⽤⼀个函数,内部判断实现。⾃⼰封装组件库经常看到

myEvent = {
    stop(e) {
        if (typeof e.preventDefault() === "function") {
            e.preventDefault();
        }
        if (typeof e.stopPropagation() === "function") {
            e.stopPropagation();
        }
        //for IE
        if (typeof e.returnValue === "boolean") {
            e.returnValue = false;
        }
        if (typeof e.cancelBubble === "boolean") {
            e.cancelBubble = true;
        }
    },
    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;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 八、工厂模式

提供创建对象的接⼝,把成员对象的创建⼯作转交给⼀个外部对象,好处在于消除对象之间的耦合(也就是相互影响)。

常⻅的例⼦,我们的弹窗,message,对外提供的api,都是调⽤api,然后新建⼀个弹窗或者Message的实例,就是典型的⼯⼚模式。

Last Updated: 3/29/2020, 2:48:05 PM