# 前端设计模式

设计模式(Design Pattern)是⼀套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

任何事情都有套路,设计模式,就是写代码中的常见套路, 有些写法我们日常都⼀直在使用,下面我们来介绍⼀下。

# 订阅/发布模式 (观察者)

pub/sub 这个应该大家用到最广的设计模式了,

在这种模式中,并不是⼀个对象调用另⼀个对象的方法,而是⼀个对象订阅另⼀个对象的 特定活动并在 状态改变后获得通知。订阅者因此也成为观察者,而被观察的对象成为发布者或者主题。当发生了⼀个 重要事件时候 发布者会通知(调用)所有订阅者并且可能经常已事件对象的形式传递消息。

class Event {
    constructor() {
        this.callbacks = {}
    }
    $off(name) {
        this.callbacks[name] = null
    }
    $emit(name, arg) {
        // 触发
        const cbs = this.callbacks[name];
        if (cbs) {
            cbs.forEach(c => {
                c.call(this, arg)
            });
        }
    }
    $on(name, fn) {
        // 监听
        (this.callbacks[name] || (this.callbacks[name] = [])).push(fn)
    }
}

let event = new Event();
event.$on('e1', function (arg) {
    console.log('e1', arg);
})
event.$emit('e1', { name: 'zhao' })

vue中的emit on源码 大概也是这个样子

https://github.com/vuejs/vue/blob/dev/src/core/instance/events.js#L54

# 单例模式

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

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

<!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>

应用场景

我们再element中的弹窗代码中,可以看到单例模式的实际案例 保证全局唯⼀性 https://github.com/ElemeFE/element/blob/dev/packages/message-box/src/main.js#L79

# 策略模式

策略模式的定义:定义⼀系列的算法,把他们⼀个个封装起来,并且使他们可以相互替换。

策略模式的目的就是将算法的使用算法的实现分离开来。

⼀个基于策略模式的程序至少由两部分组成。

第⼀个部分是⼀组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。

第⼆个部分是环境类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

使用策略模式

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 ){
 return strategies[ level ]( salary );
};
console.log( calculateBonus( 'S', 20000 ) );// 输出:80000
console.log( calculateBonus( 'A', 10000 ) );// 输出:30000

# 代理模式

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

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

图片懒加载的方式:先通过⼀张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');

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

var synchronousFile = function (id) {
    console.log('开始同步文件,id 为: ' + id);
};
var proxySynchronousFile = (function () {
    var cache = [], // 保存⼀段时间内需要同步的 ID
        timer; // 定时器
    return function (id) {
        cache.push(id);
        if (timer) { // 保证不会覆盖已经启动的定时器
            return;
        }
        timer = setTimeout(function () {
            synchronousFile(cache.join(','));
            clearTimeout(timer); // 清空定时器
            timer = null;
            cache.length = 0; // 清空 ID 集合
        }, 2000);
    }// 2 秒后向本体发送需要同步的 ID 集合
})();
var checkbox = document.getElementsByTagName('input');
for (var i = 0, c; c = checkbox[i++];) {
    c.onclick = function () {
        if (this.checked === true) {
            proxySynchronousFile(this.id);
        }
    }
}

# 中介者模式

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

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

中介者模式适用的场景:例如购物车需求,存在商品选择表单、颜⾊选择表单、购买数量表单等等,都会触发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
export const connect = (
  mapStateToProps = (state) => state,
  mapDispatchToProps = {}
) => (WrapComponent) => {
  return class ConnectComponent extends React.Component {
    static contextTypes = {
      store: PropTypes.object,
    };
    constructor(props, context) {
      super(props, context);
      this.state = {
        props: {},
      };
    }
    componentDidMount() {
      const { store } = this.context;
      // 当前状态 update 后, 放入监听器中, 用于下⼀次的更新(每次 dispatch 后会执行subscribe 中的所有函数)
      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}></WrapComponent>;
    }
  };
};

假设我们在编写⼀个飞机大战的游戏,随着经验值的增加,我们操作的飞机对象可以升级成更厉害的飞机,⼀开始这些飞机只能发射普通的子弹,升到第⼆级时可以发射导弹,升到第三级时可以发射原子弹。

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;
    }
};

比如页面中有⼀个登录 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;

使用装饰器

var showLogin = function () {
    console.log('打开登录浮层');
}
var log = function () {
    console.log('上报标签为: ' + this.getAttribute('tag'));
}
showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin;

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

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

# 外观模式

外观模式即让多个方法⼀起被调用

涉及到兼容性,参数支持多格式,有很多这种代码,对外暴露统⼀的api,比如上面的$on 支持数组, $off参数支持多个情况, 对面只用⼀个函数,内部判断实现

自己封装组件库 经常看到

myEvent = {
    stop: function (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;
        }
    }
}

# 工厂模式

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

常见的例子,我们的弹窗,message,对外提供的api,都是调用api,然后新建⼀个弹窗或者Message的实例,就是典型的工⼚模式

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 () {
        Notification.close(id, userOnClose);
    };
    instance = new NotificationConstructor({
        data: options
    });
    if (isVNode(options.message)) {
        instance.$slots.default = [options.message];
        options.message = 'REPLACED_BY_VNODE';
    }
    instance.id = id;
    instance.$mount();
    document.body.appendChild(instance.$el);
    instance.visible = true;
    instance.dom = instance.$el;
    instance.dom.style.zIndex = PopupManager.nextZIndex();
    let verticalOffset = options.offset || 0;
    instances.filter(item => item.position === position).forEach(item => {
        verticalOffset += item.$el.offsetHeight + 16;
    });
    verticalOffset += 16;
    instance.verticalOffset = verticalOffset;
    instances.push(instance);
    return instance;
};

https://github.com/ElemeFE/element/blob/dev/packages/notifification/src/main.js#L11

# 建造者模式

和工厂模式相比,参与了更多创建的过程 或者更复杂

var Person = function (name, work) {
    // 创建应聘者缓存对象
    var _person = new Human();
    // 创建应聘者姓名解析对象
    _person.name = new Named(name);
    // 创建应聘者期望职位
    _person.work = new Work(work);
    return _person;
};
var person = new Person('xiao ming', 'code');
console.log(person)

# 迭代器模式

迭代器模式是指提供⼀种方法顺序访问⼀个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

这个用的就太多了 each map啥乱遭的

var each = function (ary, callback) {
    for (var i = 0, l = ary.length; i < l; i++) {
        callback.call(ary[i], i, ary[i]);
    }
};
each([1, 2, 3], function (i, n) {
    alert([i, n]);
})

# 享元模式

享元(flyweight)模式是⼀种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。 如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在 JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了⼀件非常有意义的事情。

假设有个内衣工⼚,目前的产品有 50 种男式内衣和 50 种女士内衣,为了推销产品,工⼚决定生产⼀些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50个男模特和50个女模特,然后让他们每 人分别穿上⼀件内衣来拍照。

var Model = function (sex, underwear) {
    this.sex = sex;
    this.underwear = underwear;
};
Model.prototype.takePhoto = function () {
    console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};
for (var i = 1; i <= 50; i++) {
    var maleModel = new Model('male', 'underwear' + i);
    maleModel.takePhoto();
};
for (var j = 1; j <= 50; j++) {
    var femaleModel = new Model('female', 'underwear' + j);
    femaleModel.takePhoto();
};

如上所述,现在⼀共有 50 种男内 衣和 50 种女内衣,所以⼀共会产生 100 个对象。如果将来生产了10000 种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。 下面我们来考虑⼀下如何优化这个场景。虽然有 100 种内衣,但很显然并不需要 50 个男 模特和 50 个女模特。其实男模特和女模特各自有⼀个就⾜够了,他们可以分别穿上不同的内衣来拍照。

/*只需要区别男女模特
那我们先把 underwear 参数从构造函数中 移除,构造函数只接收 sex 参数*/
var Model = function (sex) {
    this.sex = sex;
};
Model.prototype.takePhoto = function () {
    console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};
/*分别创建⼀个男模特对象和⼀个女模特对象*/
var maleModel = new Model('male'),
    femaleModel = new Model('female');
/*给男模特依次穿上所有的男装,并进行拍照*/
for (var i = 1; i <= 50; i++) {
    maleModel.underwear = 'underwear' + i;
    maleModel.takePhoto();
};
/*给女模特依次穿上所有的女装,并进行拍照*/
for (var j = 1; j <= 50; j++) {
    femaleModel.underwear = 'underwear' + j;
    femaleModel.takePhoto();
};
//只需要两个对象便完成了同样的功能
  • 内部状态存储于对象内部
  • 内部状态可以被⼀些对象共享
  • 内部状态独⽴于具体的场景,通常不会改变
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享

性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系 统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别 通常只有男女两种,所以该内衣⼚商最多只需要 2 个对象。

# 职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成⼀条链,并沿着这条链传递该请求,直到有⼀个对象处理它为止。 职责链模式的名字非常形象,⼀系列可能会处理请求的对象被连接成⼀条链,请求在这些对 象之间依次传递,直到遇到⼀个可以处理它的对象,我们把这些对象称为链中的节点

假设我们负责⼀个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。 公司针对支付过定金的用户有⼀定的优惠政策。在正式购买后,已经支付过 500 元定金的用 户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不⼀定保证能买到。

var order = function (orderType, pay, stock) {
    if (orderType === 1) { // 500 元定金购买模式
        if (pay === true) { // 已支付定金
            console.log('500 元定金预购, 得到 100 优惠券');
        } else { // 未支付定金,降级到普通购买模式
            if (stock > 0) { // 用于普通购买的手机还有库存
                console.log('普通购买, 无优惠券');
            } else {
                console.log('手机库存不⾜');
            }
        }
    } else if (orderType === 2) {
        if (pay === true) { // 200 元定金购买模式
            console.log('200 元定金预购, 得到 50 优惠券');
        } else {
            if (stock > 0) {
                console.log('普通购买, 无优惠券');
            } else {
                console.log('手机库存不⾜');
            }
        }
    } else if (orderType === 3) {
        if (stock > 0) {
            console.log('普通购买, 无优惠券');
        } else {
            console.log('手机库存不⾜');
        }
    }
};
order(1, true, 500); // 输出: 500 元定金预购, 得到 100 优惠券

现在我们采用职责链模式重构这段代码,先把 500 元订单、200 元订单以及普通购买分成 3 个函数。接下来把 orderType、pay、stock 这 3 个字段当作参数传递给 500 元订单函数,如果该函数不符合处理条件,则把这个请求传递给后面的 200 元订单函数,如果 200 元订单函数依然不能处理该请求,则 继续传递请求给普通购买函数。

通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,假如某天网站运营人员 又想出了支持 300 元定金购买,那我们就在该链中增加⼀个节点即可

var order300 = function(){
// 具体实现略
};
chainOrder300= new Chain( order300 );
chainOrder500.setNextSuccessor( chainOrder300);
chainOrder300.setNextSuccessor( chainOrder200);

# 适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本 由于接口不兼容而不能工作的两个软件实体可以⼀起工作。 适配器的别名是包装器(wrapper),这是⼀ 个相对简单的模式。在程序开发中有许多这样的 场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。 这时候有两种解决办法,第⼀种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿 到的模块是⼀段别人编写的经过压缩的代码, 修改原接口就显得不太现实了。第⼆种办法是创建 ⼀个适配器,将原接口转换为客户希望的另⼀个接口,客户只需要和适配器打交道。

var googleMap = {
    show: function () {
        console.log('开始渲染⾕歌地图');
    }
};
var baiduMap = {
    display: function () {
        console.log('开始渲染百度地图');
    }
};
var baiduMapAdapter = {
    show: function () {
        return baiduMap.display();
    }
};
renderMap(googleMap); // 输出:开始渲染⾕歌地图
renderMap(baiduMapAdapter); // 输出:开始渲染百度地图

适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实 现的,也不考虑

它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够 使它们协同作用。

装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象 增加功能。装饰

者模式常常形成⼀条⻓的装饰链,而适配器模式通常只包装⼀次。代理 模式是为了控制对对象的访问,

通常也只包装⼀次。

我们设计很多插件,有默认值,也算是适配器的⼀种应用, vue的prop校验,default也算是适配器的

应用了

外观模式的作用倒是和适配器比较相似,有人把外观模式看成⼀组对象的适配器,但外观模式最显著的

特点是定义了⼀个新的接口。

# 模板方法模式

模板方法模式在⼀个方法中定义⼀个算法的⻣架,而将⼀些步骤的实现延迟到子类中。模板方法 使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤的具体实现

这个我们用的很多,vue中的slot,react中的children

# 备忘录模式

可以恢复到对象之前的某个状态,其实大家学习react或者redux的时候,时间旅行的功能,就算是备忘录模式的⼀个应用

https://zh-hans.reactjs.org/tutorial/tutorial.html#implementing-time-travel

# 总结

创建设计模式: 工厂,单例,建造者 原型

结构化设计模式:外观,适配器,代理,装饰器,享元 桥接,组合

行为型:策略,模板方法,观察者,迭代器,责任链,命令,备忘录,状态,访问者,终结者,解释器

Last Updated: 12/17/2021, 5:53:32 PM