【前端设计模式】

222 阅读7分钟

工厂模式

创建对象的一种方式,不用每次亲自创建对象,而是通过一个既定的“工厂”来生产对象。

OOP 中,默认创建对象一般是 new class ,但一些情况下用 new class 会很不方便。

let f1;
class Foo {}
if (a) f1 = Foo(x);
if (b) f2 = Foo(x, y);

此时就需要一个“工厂”,把创建者和 class 分离,符合开放封闭原则。

function create(a, b) {
  if (a) return Foo(x);
  if (b) return Foo(x, y);
}
const f1 = create(a, b);

示例

class Product {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  make() {}
  process() {}
}
const create = (name: string): Product => {
  return new Product(name);
};

常见场景

Vue 模板编译

<div>
  <span>静态文字</span>
  <span :id="hello" class="bar">{{ msg }}</span>
</div>

编译出 _createXxx JS 代码,这些就是使用工厂函数创建 vnode 。

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "静态文字"),
    _createElementVNode("span", {
      id: _ctx.hello,
      class: "bar"
    }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"])
  ]))
}

React jsx

const profile = <div>
  <img src="avatar.png" className="profile" />
  <h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>

编译之后

// 返回 vnode
const profile = React.createElement("div", null,
    React.createElement("img", { src: "avatar.png", className: "profile" }),
    React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);

其实React.createElement也是一个工厂。

class Vnode(tag, attrs, children) {
    // ...省略内部代码...
}
React.createElement =  function (tag, attrs, children) {
    return new Vnode(tag, attrs, children)
}

如果 JQuery 开放给用户的不是$,而是new JQuery(selector)
带来的问题:不方便链式操作、不宜将构造函数暴露给用户、尽量高内聚、低耦合。

declare interface Window {
  $: (selector: string) => JQuery;
}
class JQuery {
  selector: string;
  length: number;
  constructor(selector: string) {
    // 使用 slice 因为 NodeList 不是数组,没有数组的方法
    const domList = Array.prototype.slice.call(
      document.querySelectorAll(selector)
    );
    const length = domList.length; // 获取长度
    for (let i = 0; i < length; i++) {
      this[i] = domList[0];
    }
    this.selector = selector;
    this.length = length;
  }

  append(elem: HTMLElement): JQuery {
    // ...
    return this;
  }

  addClass(key: string, value: string): JQuery {
    // ...
    return this;
  }
}

window.$ = (selector) => {
  return new JQuery(selector);
};

单例模式

即对一个 只能创建一个实例,即使无论调用多少次。
例如 Vuex redux 这些全局数据存储,全局只能有一个实例。

class Store { /* get set ... */ }
const store1 = new Store()
store1.set(key, value)
const store2 = new Store()
store2.get(key) // 获取不到

示例

利用 TS 特性

class SingletonDesign {
  private constructor() {} // 外部无法初始化
  private static instance: SingletonDesign | null; // 静态属性,用于存储实例
  static getInstance(): SingletonDesign {
    if (SingletonDesign.instance == null) {
      SingletonDesign.instance = new SingletonDesign(); // 第一次调用时创建实例
    }
    return SingletonDesign.instance; // 返回实例
  }
}
const s1 = SingletonDesign.getInstance()
const s2 = SingletonDesign.getInstance()
console.log(s1 === s2) // true

使用闭包

function genGetInstance() {
  let instance; // 闭包
  class Singleton {}
  return () => {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  };
}
const getInstance = genGetInstance();

const s1 = getInstance();
const s2 = getInstance();
console.log(s1 === s2); // true

结合模块化语法

let instance: null | Single = null;
class Single {}
const initSingle = () => {
  if (instance === null) {
    instance = new Single();
  }
  return instance;
};
export default initSingle;

观察者模式

前端最常用的一个设计模式,也是 UI 编程最重要的思想;
例如你点奶茶,此时你并不需要在吧台坐等,等做好了服务员会叫你。

场景

  • 定时器
  • Promise
  • Vue React 的生命周期
  • Vue 组件更新过程

DOM 事件

const $btn = $("#btn");
$btn.click(function () {
  console.log("click");
});

Vue watch

{
    data() {
        name: 'zhangsan'
    },
    watch: {
        name(newVal, val) {
            console.log(newValue, val)
        }
    }
}

Nodejs stream

const fs = require('fs')
const readStream = fs.createReadStream('./file.txt') 
let length = 0
readStream.on('data', function (chunk) {
    length += chunk.toString().length
})
readStream.on('end', function () {
    console.log(length)
})

Nodejs readline

const readline = require('readline');
const fs = require('fs')
const rl = readline.createInterface({
    input: fs.createReadStream('./data/file1.txt')
})
let lineNum = 0
rl.on('line', function(line){
    lineNum++
})
rl.on('close', function() {
    console.log('lineNum', lineNum)
})

MutationObserver

<div id="container">
    <p>A</p>
    <p>B</p>
</div>

function callback(records: MutationRecord[], observer: MutationObserver) {
    for (let record of records) {
        console.log('record', record)
    }
}
const observer = new MutationObserver(callback)

const containerElem = document.getElementById('container')
const options = {
    attributes: true, // 监听属性变化
    attributeOldValue: true, // 变化之后,记录旧属性值
    childList: true, // 监听子节点变化(新增删除)
    characterData: true, // 监听节点内容或文本变化
    characterDataOldValue: true, // 变化之后,记录旧内容
    subtree: true, // 递归监听所有下级节点
}

// 开始监听
observer.observe(containerElem!, options)

// 停止监听
// observer.disconnect()

代码模拟

// 被观察者
class Subject {
  private state: string = "init";
  private observers: Observer[] = []; // 观察者列表
  getState() {
    return this.state;
  }
  setState(newState: string) {
    this.state = newState;
    this.notify(); // 通知所有观察者
  }
  // 添加观察者
  attach(observer: Observer) {
    this.observers.push(observer);
  }
  // 通知所有观察者
  private notify() {
    for (const observer of this.observers) {
      observer.update(this.state);
    }
  }
}
// 观察者
class Observer {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  update(state: string) {
    console.log(`${this.name} update, state is ${state}`);
  }
}

const sub = new Subject(); // 被观察者
const observer1 = new Observer("A");
const observer2 = new Observer("B");
sub.attach(observer1);
sub.attach(observer2);
sub.setState("done"); // 更新状态,触发观察者 update

发布订阅模式

观察者模式的另一个版本,用于实现对象之间的松耦合通信;
在该模式中,存在一个或多个发布者(Publishers)和一个或多个订阅者(Subscribers);
发布者负责发布消息,而订阅者负责订阅感兴趣的消息并在接收到消息时做出相应的处理;
postMessage、Nodejs 多进程通讯、WebWorker

对比观察者模式
观察者模式 中间无媒介 如 addEventListener 绑定事件
发布订阅模式 中间有媒介 如 event 自定义事件


在这里插入图片描述

场景

自定义事件 Vue2 实例本身支持,Vue3 推荐使用 mitt

import mitt from 'mitt'
const emitter = mitt() // 单例
export default emitter
emitter.on('change', () => {
    console.log('change')
})
emitter.emit('change')

mitt 没有 once ,推荐 event-emitter

import eventEmitter from 'event-emitter' // 安装TS类型声明 @types/event-emitter
const emitter = eventEmitter()
emitter.once('change', (value: string) => {
    console.log('change', value)
})
emitter.emit('change', '张三')

组件销毁之前及时 off 避免内存泄漏

created() {
    emitter.on('change', this.fn)
},
beforeUnmount() {
    emitter.off('change', this.fn)
}

代码模拟

class PubSub {
  subscribers = {} // 存放订阅者
  /**
   * 订阅
   * @param eventName 事件名称
   * @param callback
   */
  on(eventName, callback) {
    if (!this.subscribers[eventName]) {
      this.subscribers[eventName] = [] // 初始化
    }
    this.subscribers[eventName].push(callback)
  }
  /**
   * 取消订阅
   * @param eventName 事件名称
   * @param callback
   */
  off(eventName, callback) {
    if (this.subscribers[eventName]) {
      // 订阅函数的索引
      const index = this.subscribers[eventName].findIndex(item => item === callback)
      if (index !== -1) {
        this.subscribers[eventName].splice(index, 1)
      }
    }
  }
  // 发布
  emit(eventName, data) {
    if (this.subscribers[eventName]) {
      this.subscribers[eventName].forEach(callback => {
        callback(data)
      })
    }
  }
}

const pubsub = new PubSub()
function callback1(data) {
  console.log('订阅1收到', data)
}
function callback2(data) {
  console.log('订阅2收到', data)
}
function callback3(data) {
  console.log('订阅3收到', data)
}
pubsub.on('message', callback1) // 订阅消息
pubsub.on('advertisement', callback2) // 订阅广告
pubsub.on('notice', callback2) // 订阅通知
pubsub.emit('message', '今天天气状况不错,适合出游') // 发布消息
pubsub.emit('advertisement', '全场5折') // 发布广告
pubsub.emit('notice', '明天放假一天') // 发布通知
pubsub.off('message', callback2) // 取消订阅

装饰器模式

允许向一个现有的对象添加新的功能,同时又不改变其结构;
这种类型的设计模式属于结构型模式,作为现有的类的一个包装,动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式相比生成子类更为灵活。
简单地说: 允许向一个现有的对象添加新的功能,同时又不改变其结构,目标是分离和解耦的;

例如,手机上套一个壳可以保护手机,壳上粘一个指环,这就是一种装饰

function decorate(phone) {
    phone.fn3 = function () {
        console.log('指环')
    }
}
const phone = {
    name: 'iphone12',
    fn1() {}
    fn2() {}
}
const newPhone = decorate(phone)

在这里插入图片描述

class Circle {
    draw() {
        console.log('画一个圆')
    }
}

class Decorator {
    private circle: Circle
    constructor(circle: Circle) {
        this.circle = circle
    }
    draw() {
        this.circle.draw()
        this.setBorder()
    }
    private setBorder() {
        console.log('设置边框颜色')
    }
}

const circle = new Circle()
circle.draw()

const decorator = new Decorator(circle)
decorator.draw()

ES 引入了 Decorator 语法,tsconfig.json 配置 experimentalDecorators: true

// 装饰器
function testable(target: any) {
    target.isTestable = true
}

@testable
class Foo {
    static isTestable?: boolean
}

console.log(Foo.isTestable) // true

也可以进行传参

// 装饰器工厂函数
function testable(val: boolean) {
    // 装饰器
    return function (target: any) {
        target.isTestable = val
    }
}

@testable(false)
class Foo {
    static isTestable?: boolean
}

console.log(Foo.isTestable) // false

装饰类

/**
 * @param instance 实例
 * @param key 属性名
 */
function instanceDecorator(instance: InstanceType<any>, key: any) {}

/**
 * @param target 实例
 * @param key 属性名
 * @param descriptor 属性描述符
 */
function readOnly(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.writable = false
}

// 传参
function configurable(val: boolean) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = val
  }
}

class PropertyDescribe {
  @instanceDecorator // 等同于 instanceDecorator(this, "_name");
  _name = '张三'
  _age = 20

  // 等同于 Object.defineProperty(this, "getName", { writable: false });
  @readOnly
  getName() {
    return this._name
  }

  // 等同于 Object.defineProperty(this, "getAge", { configurable: false });
  @configurable(false)
  getAge() {
    return this._age
  }
}

const pdd = new PropertyDescribe()

// @ts-ignore
console.log(Object.getOwnPropertyDescriptor(pdd.__proto__, 'getName'))
// @ts-ignore
console.log(Object.getOwnPropertyDescriptor(pdd.__proto__, 'getAge'))

同样 react-redux 也采用了装饰器模式

import { connect } from 'react-redux'
export default TodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)
import { connect } from 'react-redux'
@connect(mapStateToProps, mapDispatchToProps)
export default TodoList extends React.Component { }

AOP 面向切面编程实现 log 打印

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log(descriptor);
  const oldValue = descriptor.value; // business 函数
  console.log(oldValue);

  // 重新定义 business 函数
  descriptor.value = function () {
    console.log(`记录日志...`);
    return oldValue.apply(this, arguments);
  };
}

class Business {
  @log // 不影响业务功能的代码,只加 log 的 “切面”
  business() {
    console.log("业务功能");
  }
}

const bs = new Business();
bs.business();

代理模式

为其他对象提供一种代理,以控制对这个对象的访问。

直接访问对象时带来的问题:

在面向对象系统中,有些对象由于某些原因(如对象创建开销大,或某些操作需要安全控制,或需要进程外的访问等等...),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上对此对象的访问层。

在这里插入图片描述

例如,你通过房产中介买房子,中介就是一个代理。

通过代理类在调用真实类之前做一些处理

class RealImg {
  fileName;
  constructor(fileName) {
    this.fileName = fileName;
  }
  display() {
    console.log("display...", this.fileName);
  }
}
// 大致原理:通过代理类,在调用真实类之前做一些处理
class IProxy {
  readImg;
  constructor(fileName) {
    this.readImg = new RealImg(fileName);
  }
  display() {
    // dome something
    this.readImg.display();
  }
}
// 使用代理
const proxImg = new IProxy("proxy.png");
proxImg.display();

场景 - 代理模式在前端十分很常用,诸如以下例子:

DOM 事件代理

<div id="div1">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
</div>
<button>点击增加一个 a 标签</button>
<script>
    var div1 = document.getElementById('div1')
    div1.addEventListener('click', function (e) {
        var target = e.target
        if (e.nodeName === 'A') {
            alert(target.innerHTML)
        }
    })
</script>

Webpack devServer

// webpack.config.js
module.exports = {
  // 其他配置...
  devServer: {
    proxy: {
      '/api': 'http://localhost:8081',
    },
  },
};

nginx 反向代理

server {
    listen   8000;
    location / {
        proxy_pass http://localhost:8001;
    }
    location /api/ {
        proxy_pass http://localhost:8002;
        proxy_set_header Host $host;
    }
}

Vue3 reactive

function reactive(target = {}) {
  // 不是对象或数组
  if (typeof target !== 'object' || target === null) return target
  // 代理配置项
  const observed = new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      return reactive(result) // 深度监听
    },
    set(target, key, val, receiver) {
      if (val === target[key]) return true // 判重
      return Reflect.set(target, key, val, receiver)
    },
    deleteProperty(target, key) {
      return Reflect.deleteProperty(target, key)
    }
  })
  return observed
}

const data = {
  realName: 'zhangsan'
}
const user = reactive(data)
user.realName = 'lisi'
console.log(user.realName) // lisi

职责链模式

顾名思义,就是一步操作可能分位多个职责角色来完成,把这些角色都分开,然后用一个链串起来。这样就将请求者和处理者、包括多个处理者之间进行了分离;

前端最常见的就是链式操作

如:promise.then、Jquery 链式

Query 链式操作

$("#div1").show().css("color", "red").append($("#p1"));

Promise 链式操作

function loadImg(src: string) {
  const promise = new Promise((resolve, reject) => {
    const img = document.createElement("img");
    img.onload = () => {
      resolve(img);
    };
    img.onerror = () => {
      reject("图片加载失败");
    };
    img.src = src;
  });
  return promise;
}

const src = "//www.baidu.com/img/flexible/logo/pc/result.png";
const result = loadImg(src);
result
  .then((img: HTMLImageElement) => {
    console.log("img.width", img.width);
    return img;
  })
  .then((img: HTMLImageElement) => {
    console.log("img.height", img.height);
  })
  .catch((err) => {
    console.log(err);
  });

策略模式

主要解决多个 if...else 或者 switch...case 的问题,每种情况分成多种策略,分别实现。

class User {
    private type: string
    constructor(type: string) {
        this.type = type
    }
    buy() {
        const { type } = this
        if (type === 'ordinary') {
            // ...
        }
        if (type === 'member') {
            // ...
        }
        if (type === 'vip') {
            // ...
        }
    }
}

const u1 = new User('ordinary')
u1.buy()
const u2 = new User('member')
u2.buy()
const u3 = new User('vip')
u3.buy()

使用策略模式

interface IUser {
    buy: () => void
}

class OrdinaryUser implements IUser {
    buy() {
        // ...
    }
}

class MemberUser implements IUser {
    buy() {
        // ...
    }
}

class VipUser implements IUser {
    buy() {
        // ...
    }
}

const u1 = new OrdinaryUser()
u1.buy()
const u2 = new MemberUser()
u2.buy()
const u3 = new VipUser()
u3.buy()

适配器模式

我们需要一个对象的 API 提供能力,但它的格式不一定完全适合我们的格式要求。这就要转换一下

如 Vue computed

{
    data() {
        return {
            userList: [
                { id: 1, name: '张三' },
                { id: 2, name: '李四' },
                { id: 3, name: '王五' },
            ]
        }
    },
    computed: {
        userNameList() {
            this.userList.map(user => user.name)
        }
    }
}

MVC

  • View 传送指令到 Controller
  • Controller 完成业务逻辑后,要求 Model 改变状态
  • Model 将新的数据发送到 View,用户得到反馈

在这里插入图片描述

MVVM

MVVM 对标 Vue 即可,View 和 Model 通过 viewModel 实现互动

  • View 即 Vue template(DOM / 模板)
  • Model 即 Vue data(vue 组件中的 data 或者vuex )
  • VM 即 Vue 其他核心功能(连接层、可以理解为Vue实例中的监听、指令、方法等),负责 View 和 Model 通讯

在这里插入图片描述 View 通过事件监听等修改 Model,Model 通过指令修改 View 在这里插入图片描述