Skip to content

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>

备忘录模式

可以恢复到对象之前的某个状态(记录用户操作的数组,支持撤销和回退)

简单的备忘录直接使用数组实现

复杂的备忘录需要根据每个操作,获取反向操作并执行

总结

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

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

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


Last update: November 9, 2024