设计模式

设计模式总共有 23 种,但在前端畛域其实没必要全副都去学习,毕竟大部分的设计模式是在 JavaScript 中占的比重并不是那么大,本文会列举出一些 JavaScript 常见的、容易被忽视的设计模式,不过还是有必要先简略理解一下设计模式相干的概念.

设计模式是什么?

先举个形象的例子,比方当初正在考试而且恰好在考数学,实际上每道数学题目都对应着一种或多种解决公式(如和三角形相干的勾股定理),而这些解决公式是通过数学家钻研、推导、总结好的,咱们只须要把 题目已有公式 对应上就很容易解决问题,而 设计模式 也是如此,只不过是它是绝对于 软件设计畛域 而言的.

设计模式(Design pattern) 是一套被重复应用、通过分类、代码设计教训的总结,简略来说设计模式就是为了解决 软件设计畛域 不同场景下相应问题的 解决方案.

设计准则(SOLID)

SOLID 实际上指的是五个根本准则,但在前端畛域波及到最多的是依然是后面两条:

  • 繁多性能准则(Single Responsibility Principle)
  • 凋谢关闭准则(Opened Closed Principle)
  • 里式替换准则(Liskov Substitution Principle)
  • 接口隔离准则(Interface Segregation Principle)
  • 依赖反转准则(Dependency Inversion Principle)

设计模式的类型

次要分为三个类型:

  • 创立型

    • 次要用于解耦 对象的实例化 过程,即用于创建对象,如对象实例化
    • 本文次要蕴含:简略工厂模式、形象工厂模式、单例模式、原型模式
  • 行为型

    • 次要用于优化不同 对象接口 间的构造关系,如把 对象 联合在一起造成一个更大的构造
    • 本文次要蕴含:装璜器模式、适配器模式、代理模式
  • 结构型

    • 次要用于定义 对象 如何交互、划分责任、设计算法
    • 本文次要蕴含:策略模式、状态模式、观察者模式、公布订阅模式、迭代器模式

    创立型设计模式

    设计模式的外围是辨别逻辑中的 可变局部不变局部,并使它们进行拆散,从而达到使变动的局部易扩大、不变的局部稳固.

    工厂模式

    简略工厂模式

    外围就是创立一个对象,这里的 可变局部参数不变局部共有属性.

举例:通过不同职级的员工创立员工相干信息,须要蕴含 name、age、position、job 等信息.

实现形式一:

  • 外围就是 可变局部 默认 参数化

    function Staff(name, age, position, job) {  this.name = name;  this.age = age;  this.position = position;  this.job = job;}const developer = new Staff('zs', 18, 'develoment', ['写 bug', '改 bug', '摸鱼']);const productManager = new Staff('ls', 30, 'manager', ['提需要', '改需要', '面向 PPT 开发']);

    实现形式二:

  • 实际上在实现形式一中的 job 局部是和 position 是互相关联的,能够认为 job 局部是 不变的,因而能够依据 position 内容的内容来主动匹配 job

    function Staff(name, age, position, job) {  this.name = name;  this.age = age;  this.position = position;  this.job = job;}function StaffFactory(name, age, position){  let job = []  switch (position) {      case 'develoment':          job = ['写 bug', '改 bug', '摸鱼'];          break;      case 'manager':          job = ['提需要', '改需要', '面向 PPT 开发'];          break;      ...  }  return new Staff(name, age, position, job);}const developer = StaffFactory('zs', 18, 'developer');const productManager = StaffFactory('ls', 30, 'manager');

    形象工厂模式

    这个模式最显眼的就是 形象 两个字了,在如 Java 语言当中存在所谓的 抽象类,这个抽象类外面的所有属性和办法都没有具体实现,只有单纯的定义,而继承这个抽象类的子类必须要实现其对应的形象属性和形象办法.

JavaScript 中没有这样的间接定义,不过依据下面的形容其实咱们能够把它映射到 typescript 中的 interface 接口,了解到这其实让我联想到了 vue.js 中的 自定义渲染器,预留的自定义渲染器的各个办法目标就是实现跨平台的渲染形式

// 文件地位:packages\runtime-core\src\renderer.tsexport function createRenderer<  HostNode = RendererNode,  HostElement = RendererElement>(options: RendererOptions<HostNode, HostElement>) {  return baseCreateRenderer<HostNode, HostElement>(options)}// 文件地位:packages\runtime-core\src\renderer.ts// RendererOptions 就是一个 Interface 接口export interface RendererOptions<  HostNode = RendererNode,  HostElement = RendererElement> {  patchProp(    el: HostElement,    key: string,    prevValue: any,    nextValue: any,    isSVG?: boolean,    prevChildren?: VNode<HostNode, HostElement>[],    parentComponent?: ComponentInternalInstance | null,    parentSuspense?: SuspenseBoundary | null,    unmountChildren?: UnmountChildrenFn  ): void  insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void  remove(el: HostNode): void  createElement(    type: string,    isSVG?: boolean,    isCustomizedBuiltIn?: string,    vnodeProps?: (VNodeProps & { [key: string]: any }) | null  ): HostElement  createText(text: string): HostNode  createComment(text: string): HostNode  setText(node: HostNode, text: string): void  setElementText(node: HostElement, text: string): void  parentNode(node: HostNode): HostElement | null  nextSibling(node: HostNode): HostNode | null  querySelector?(selector: string): HostElement | null  setScopeId?(el: HostElement, id: string): void  cloneNode?(node: HostNode): HostNode  insertStaticContent?(    content: string,    parent: HostElement,    anchor: HostNode | null,    isSVG: boolean,    start?: HostNode | null,    end?: HostNode | null  ): [HostNode, HostNode]}

接下来咱们将以上的 typescript 的模式转变成 JavaScript 模式的形象模式:

// 形象 Render 类class Renderer {  patchProp(    el,    key,    prevValue,    nextValue,    isSVG,    prevChildren,    parentComponent,    parentSuspense,    unmountChildren  ) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  insert(el, parent, anchor) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  remove(el) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  createElement(type, isSVG, isCustomizedBuiltIn, vnodeProps) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  createText(text) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  createComment(text) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  setText(node, text) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  setElementText(node, text) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  parentNode(node) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  nextSibling(node) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  querySelector(selector) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  setScopeId(el, id) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  cloneNode(node) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }  insertStaticContent(content, parent, anchor, isSVG, start, end) {    throw Error('形象工厂办法不能间接应用,你须要将我重写!!!');  }}// 具体渲染函数的实现class createRenderer extends Renderer{    // 待实现的渲染器办法    ...}

单例模式

外围就是通过屡次 new 操作进行实例化时,可能保障创立 实例对象唯一性.

vuex 中的单例模式

其实,vuex 中就应用到了 单例模式,代码自身比较简单,当 install 办法被屡次调用时,就会失去一个错误信息,并不会屡次向 Vue 中混入 vuex 中自定义的内容:

实现一个单例模式

这里举个封装 localStorage 办法的例子,并提供给内部对应的创立办法,如下:

let storageInstance = null;class Storage {    getItem(key) {        let value = localStorage.getItem(key);        try {            return JSON.parse(value);        } catch (error) {            return value;        }    }    setItem(key, value) {        try {            localStorage.setItem(JSON.stringify(value));        } catch (error) {            // do something            console.error(error);        }    }}// 单例模式export default function createStorage(){    if(!storageInstance){        storageInstance = new Storage();    }    return storageInstance;}

原型模式

JavaScript 中原型模式是很常见的,JavaScript 中实现的 继承 或者叫 委托 兴许更适合,因为它不等同于如 Java 等语言中的继承,毕竟 JavaScript继承 是基于原型(prototype)来实现.

class Person {    say() {        console.log(`hello, my name is ${this.name}!`);    }    eat(foodName) {        console.log(`eating ${foodName}`);    }}class Student extends Person {    constructor(name) {        super();        this.name = name;    }}const zs = new Student('zs');const ls = new Student('ls');console.log(zs.say === ls.say);// Java 中是不相等的, JavaScript 中是相等的console.log(zs.eat === ls.eat);// Java 中是不相等的, JavaScript 中是相等的

vue2 中的原型模式

文件地位:\src\core\instance\lifecycle.js

结构型设计模式

装璜器模式

外围是在不扭转原 对象/办法 的根底上,通过对其进行包装拓展,使原有 对象/办法 能够满足更简单的需要.

装璜器实质

装璜器模式实质上就是 函数的传参和调用,通过函数为已有 对象/办法 进行扩大,而不必批改原对象/办法,满足 凋谢关闭准则.

通过配置 babel 通过将 test.js 转为为 bable_test.js 用来查看装璜器的实质:

babel.config.json

{  "presets": [    [      "@babel/preset-env",      {        "targets": {          "node": "current"        }      }    ]  ],  "plugins": [    ["@babel/plugin-proposal-decorators", { "legacy": true }],    ["@babel/plugin-proposal-class-properties", { "loose": true }]  ]}

test.js

// 定义装璜器function decoratorTest(target) {  console.log(target);}// 应用装璜器,装璜 Person 类@decoratorTestclass Person {  say() {}  eat() {}}

执行 babel test.js --out-file babel_test.js 命令是生成 babel_test.js

"use strict";var _class;function decoratorTest(target) {  console.log(target);}let Person = decoratorTest(_class = class Person {  say() {}  eat() {}}) || _class;

React 中的装璜器模式 —— HOC 高阶组件

高阶组件 是参数为 组件,返回值为新组件的 函数,在 ReactHOC 通常用于复用组件公共逻辑.

// TodoList 组件class TodoList extends React.Component {}// HOC 函数function WrapContainer(Comp) {  return (    <div style={{ border: "1px solid red", padding: 10 }}>      <Comp title="todo" />    </div>  );}// HOC 装璜 TodoList 组件,为 TodoList 组件包裹红色边框const newTodoList = WrapContainer(TodoList);

适配器模式

适配器模式实质就是 让本来不兼容的性能可能失效,防止大规模批改代码,对外提供对立应用.

Axios 中的适配器

通过观察 Axios 的目录构造,很容就发现其应用了适配器模式:

其实 Axios 中的 adapters 次要目标是依据以后运行时环境,向外返回对应的适配器 adapter,而这个适配器要做的其实就是兼容 web 浏览器环境和 node 环境的 http 申请,保障对外裸露的依然是对立的 API 接口

代理模式

代理模式顾名思义就是 不能间接拜访指标对象,须要通过代理器来实现拜访,通常是为了晋升性能、保障平安等.

事件代理

事件代理是很常见的性能优化伎俩之一,react 的事件机制也采纳了事件代理的形式(篇幅无限可自行理解),这里演示简略的 JavaScript 事件代理:

<div id="container">  <p>this number is 1</p>  <p>this number is 2</p>  <p>this number is 3</p>  <p>this number is 4</p>  <p>this number is 5</p></div><script>  const container = document.querySelector("#container");  container.addEventListener("click", function (e) {    alert(e.target.textContent);  });</script>

Vue 中的代理 Proxy

Vue.js 3 中通过 Proxy 实现了对数据的代理,任何读取、设置的操作都会被 代理对象handlers 拦挡到,从而实现 Vue 中的 tracktrigger


行为型设计模式

策略模式

策略模式实际上就是定义一系列的算法,将单个性能封装起来,并且对扩大凋谢.

举个例子

如果咱们须要为某个游乐场的门票价格做差异化询价,次要人员类型分为 儿童、成年人、老年人 三种,其对应的门票折扣为 8折、9折、8.5折

if-else 代码一把梭

毛病:无论哪种人员类型的折扣变动,都须要批改 finalPrice 函数,不合乎对 对批改关闭

function finalPrice(type, price) {  if (type === "child") {    // do other thing    return price * 0.8;  }  if (type === "adult") {    // do other thing    return price * 0.9;  }  if (type === "aged") {    // do other thing    return price * 0.85;  }}

繁多性能封装

毛病:若人员类型减少妇女类型,依然须要批改 finalPrice 函数,且不合乎 对扩大凋谢

function childPrice(price) {  // do other thing  return price * 0.8;}function adultPrice(price) {  // do other thing  return price * 0.9;}function agedPrice(price) {  // do other thing  return price * 0.85;}function finalPrice(type, price) {  if (type === "child") {    return childPrice(price);  }  if (type === "adult") {    return adultPrice(price);  }  if (type === "aged") {    return agedPrice(price);  }}

创立映射关系

通过映射关系,很好的将 finalPrice 和 具体的计算逻辑进行拆散,在须要扩大类型时,只须要批改 priceTypeMap 对象而不必批改对外裸露的 finalPrice 函数.

const priceTypeMap = {  child: function (price) {    // do other thing    return price * 0.8;  },  adult: function (price) {    // do other thing    return price * 0.9;  },  aged: function (price) {    // do other thing    return price * 0.85;  },};function finalPrice(type, price) {    return priceTypeMap[type](price);}

状态模式

状态模式容许一个对象在其外部状态产生扭转时,可能扭转本来的行为.

举例子

如果当初咱们须要设计一个售票机器,次要发售 巴士、火车、飞机票等,价格别离为 50、150、1000,并且可能依据残余票数决定是否可能持续购买.

通过策略模式实现外围代码逻辑

有了下面的 策略模式 的思维,立马就能够设计出如下的代码:

毛病:没有依据残余票数决定是否能够持续售卖,次要起因就在于抽离的 ticketTypeMapTicketMachine 之间的状态没有关联

const ticketTypeMap = {  bus() {    // do other thing    return 50;  },  train() {    // do other thing    return 150;  },  plane() {    // do other thing    return 1000;  },};class TicketMachine {  constructor() {    // 残余票数    this.remain = {      bus: 100,      train: 150,      plane: 200,    };  }  selling(type) {    return ticketTypeMap[type]();  }}

关联对象状态 — 函数传参

通过函数传参的形式将对象传递给指标函数,让指标函数通过该对象拜访和批改对象外部的状态.

const ticketTypeMap = {  bus(remain) {    if (remain.bus <= 0) return Error("道歉,巴士票已售完");    remain.bus--;    return 50;  },  train(remain) {    if (remain.train <= 0) return Error("道歉,火车票已售完");    remain.train--;    return 150;  },  plane(remain) {    if (remain.plane <= 0) return Error("道歉,飞机票已售完");    remain.plane--;    return 1000;  },};class TicketMachine {  constructor() {    // 残余票数    this.remain = {      bus: 100,      train: 150,      plane: 200,    };  }  selling(type) {    return ticketTypeMap[type](this.remain);  }}

关联对象状态 — 整合办法

实际上 ticketTypeMap 映射的办法和 TicketMachine 有较强的关联性,不应该独自存在,因而,能够将这个映射对象整合进 TicketMachine 当中

class TicketMachine {  constructor() {    // 残余票数    this.remain = {      bus: 100,      train: 150,      plane: 200,    };  }  ticketTypeMap = {    that: this,    bus() {      const { remain } = this.that;      if (remain.bus <= 0) return Error("道歉,巴士票已售完");      remain.bus--;      return 50;    },    train() {      const { remain } = this.that;      if (remain.train <= 0) return Error("道歉,火车票已售完");      remain.train--;      return 150;    },    plane() {      const { remain } = this.that;      if (remain.plane <= 0) return Error("道歉,飞机票已售完");      remain.plane--;      return 1000;    },  };  selling(type) {    return this.ticketTypeMap[type]();  }}

观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个指标对象,当这个指标对象的状态发生变化时,会告诉所有观察者对象,使它们可能自动更新.

vue 中的观察者模式

vue 中的响应式原理就应用了 观察者模式,咱们简略回顾一下其工作流程:

  • compile:将模板内容编译失去对应的 render 渲染函数
  • render:渲染函数执行生成 VNode,通过 patch 函数初始化视图 view

    • Observe:负责将 data 中返回的对象进行数据劫持(getter/setter),且其中会应用 Dep 来实现 watcher 的存储,相当于 被观察者
    • Dep:在触发 getter 时执行 dep.depend() 实际上执行的是 watcher.addDep(),该办法会将以后的 dep 对象保留到 Watcher,同时将以后的 watcher 通过 dep.addSub() 增加到 Dep
    • Watcher:相当于 观察者,提供对立的 update() 办法供 Dep 调用
  • data changed:响应式数据产生变更,触发数据劫持操作 setter

    • 进而执行 dep.notify() 办法,通过循环去执行 watcher.update() 办法,即执行 queueWatcher()watcher 增加到 queue 队列中
    • 最初由 scheduler 调度器 中执行 nextTick(flushSchedulerQueue) 进行异步队列刷新操作

以上过程中,显然 ObserveWatcher 就是 被观察者观察者 ,因为 Observe 中实现了对 Watcher 的收集和监听到数据状态发生变化时告诉 Watcher 更新的解决,能够认为 Dep 只是 Observe 中应用到的一个存储和派发 Watcher 的工具.

公布订阅模式

公布订阅模式有三个外围:发布者、事件核心、订阅者,且公布订阅模式中的 发布者订阅者 不能间接进行通信,必须要通过 事件核心 来对立调度.

与观察者模式的区别

实际上,公布订阅模式和观察者模式在概念上十分类似,做的事件也都统一,次要区别在于:

  • 公布订阅模式依赖于 事件核心 对立调度 发布者订阅者发布者订阅者 不间接进行通信
  • 观察者模式中的 被观察者观察者 是间接建设连贯的,被观察者 须要保留 观察者 的信息,观察者 须要提供对立的 办法 供观察者进行应用

实现公布订阅模式

vue 中的 全局事件总线(Event Bus)和 node 中的 Event Emitter,甚至是浏览器中的事件注册(addEventListener)和执行,它们都属于公布订阅模式.

上面实现一个简略的公布订阅模式:

class EventEmitter {  constructor() {    this.handlers = {};  }  on(name, handle) {    if (!this.handlers[name]) {      this.handlers[name] = [];    }    this.handlers[name].push(handle);  }  emit(name, ...args) {    if (this.handlers[name]) {      this.handlers[name].forEach((handle) => {        handle(...args);      });    }  }  off(name, handle) {    if (this.handlers[name]) {      this.handlers[name] = this.handlers[name].filter((h) => {        if (handle) return h !== handle;        return false;      });    }  }  once(name, handle) {    const onceHandle = (...args) => {      handle(...args);      this.off(name, onceHandle);    };    this.on(name, onceHandle);  }}

迭代器模式

迭代器模式是指提供一种办法程序拜访一个聚合对象中的各个元素,而又不须要裸露该对象的外部示意,外围目标就是 遍历.

JavaScript 中的遍历形式

  • Array :for...of、for...in、forEach、map、filter
  • Object :for...in
  • Map :for...of、forEach
  • Set :for...of、forEach

看起来很难有一种办法可能兼容以上几种数据结构的遍历形式,即不须要思考数据结构自身就能实现遍历的目标,但咱们能够基于 ES6Symbol.iterator 实现自定义迭代器.

Symbol.iterator 实现通用迭代

Symbol.iterator 为每一个对象定义了默认的迭代器,领有该迭代器后就能够被 for...of 循环应用.

function $each(data, handle) {  if (typeof data !== "object") throw TypeError("data should be object!");  if (!data[Symbol.iterator]) {    Object.prototype[Symbol.iterator] = function () {      let i = 0;      let keys = Reflect.ownKeys(this);      return {        next() {          const done = i >= keys.length;          return {            value: done ? undefined : keys[i++],            done,          };        },      };    };  }  for (const item of data) {    handle(item);  }}

最初

大前端的各种新技术层出不穷,很容易漠视如数据结构、设计模式等根底内容,其实看很多设计模式相干的内容,很少有讲得简略易懂的,终归是没有联合现有的框架去学习到底是如何应用起来。