当设计模式遇上前端

创建模式

单例模式

证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。 很好理解,比如说一个windows系统, 只有一个任务管理器,在开机的时候创建,后面无论怎么访问都是这个任务管理器。

abap 复制代码
class SingleDog {
  show (){
    console.log('ur a single dog');
  }
  static getInstance() {
    if (!SingleDog.instance) {
      SingleDog.instance = new SingleDog()
    }
    return SingleDog.instance
  }
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
s1 === s2

缺点:1. 违反了单一职责原则,又创建又使用。 2. 不易于扩展,没有抽象层

vuex 使用单例模式

一般情况下,一个实例vue对象中只有一个store,vuex作为vue应用下的全局状态机,在生成vue实例中初始化,所以符合单例模式。

abap 复制代码
export function install (_Vue) {
  // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的 store)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若没有,则为这个Vue实例对象install一个唯一的Vuex
  Vue = _Vue
  // 将Vuex的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue)
}

vuex 并没有在class 中做单例限制,而是在install里面限制。因此也可以说vuex没有严格符合单例模式,但一般项目中就只有一个vue实例对象。

结构模式

代理模式

代理模式,式如其名------在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。

比如科学上网

事件代理

abap 复制代码
    <!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>事件代理</title>
    </head>
    <body>
      <div id="father">
        <a href="#">链接1号</a>
        <a href="#">链接2号</a>
        <a href="#">链接3号</a>
      </div>
    </body>
    </html>

如果不适用代理,只能用for循环逐个添加。 而使用代理模式,则是通过监听父元素来实现监听所有子元素。(父元素 代理 子元素)

因为元素的事件监听有冒泡特性。(先捕获,再冒泡)

abap 复制代码
// 获取父元素
const father = document.getElementById('father')

// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
    // 识别是否是目标子元素
    if(e.target.tagName === 'A') {
        // 以下是监听函数的函数体
        e.preventDefault()
        alert(`我是${e.target.innerText}`)
    }
} )

缓存代理

目标: 用空间换时间 方法: 缓存计算的中间值,或者缓存已经计算过的值

算法题中, 递归经常使用这种方法提高效率。

abap 复制代码
function fibonacci(n) {
  if (n === 1 || n === 2) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
function getAllFibo(arr) {
  const map = {};
  const result = [];
  arr.forEach((item) => {
    let calcFibo;
    if (map[item]) {
      calcFibo = map[item]; // 代理表现,通过访问map 代替访问 fabonacci函数
    } else {
      calcFibo = fibonacci(item);
      map[item] = calcFibo;
    }
    result.push(calcFibo);
  });
  return result;
}
getAllFibo([1, 2, 3, 4, 5, 6, 3, 3, 3, 3]);

保护代理

使用Proxy构造函数,通过proxy对象来先对目标对象进行代理 如果是不可访问的对象直接返回特定信息实现保护。

abap 复制代码
const protectedObj = {
  name: 'jimmy',
  age: 18,
}
const visitedObj = new Proxy(protectedObj, {
  get(target, property, receiver) {
    if (property === 'age') {
      return 'fku';
    } else {
      return Reflect.get(target, property, receiver)
    }
  }
})
visitedObj.name; // jimmy
visitedObj.age; // fku

vue3 中使用的代理

vue3 的响应式数据就是使用 代理实现的。 为什么响应式的数据会触发页面更新,而普通数据不触发更新? 因为声明响应式数据的时候会经过一层代理,修改了set方法,每次触发修改时,会记录一层更新事件,在下一次更新时统一更新。

abap 复制代码
class RefImpl {
    constructor(value, __v_isShallow) { // 值,是否浅层ref
        this.__v_isShallow = __v_isShallow;
        this.dep = undefined;
        this.__v_isRef = true;
        this._rawValue = __v_isShallow ? value : toRaw(value);
        this._value = __v_isShallow ? value : toReactive(value);  // 判断是否为浅层ref,否则调用toReactive,方法在下面
    }
    get value() { // getter方法 获取value值
        trackRefValue(this);
        return this._value;
    }
    set value(newVal) { // setter方法 设置value值
        const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
        newVal = useDirectValue ? newVal : toRaw(newVal);
        if (hasChanged(newVal, this._rawValue)) {
            this._rawValue = newVal;
            this._value = useDirectValue ? newVal : toReactive(newVal); // 在value值更新时进行判断是否为浅层ref,否则调用toReactive
            triggerRefValue(this, newVal);
        }
    }
}

为什么模板中不用.value 而 js 代码中要写上呢,因为模板中使用了 proxy 自动 脱.value

abap 复制代码
export function proxyRefs(object) {
  return new Proxy(object, {
    // 代理的思想,如果是ref 则取ref.value
    get(target, key, recevier) {
      let r = Reflect.get(target, key, recevier)
      return r.__v_isRef ? r.value : r
    },
    // 设置的时候如果是ref,则给ref.value赋值
    set(target, key, value, recevier) {
      let oldValue = target[key]
      if (oldValue.__v_isRef) {
        oldValue.value = value
        return true
      } else {
        return Reflect.set(target, key, value, recevier)
      }
    },
  })
}

行为模式

策略模式 - 状态模式

业务上用得最多的, 比如说 根据不同的状态展示不同的样式 就可以实现一个 statusMap 函数来实现

解决大量的if-else代码 有利于代码遵循开闭原则

abap 复制代码
    function a(){} // do a
    function b(){} // do b
    function c(){} // do c
    function badDoSomething(param) {
      if (param === 'a') {
        a()
      } else if (param === 'b') {
        b()
      } else if (param === 'c') {
        c()
      }
    }
    function goodDoSomething(param) {
      const map = {
        a: a,
        b: b,
        c: c
      }
      map[param]();
    }

策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。 但策略模式中的行为函数是"潇洒"的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。

观察者模式 - 发布订阅模式

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

区别,观察者模式一般是发布者和订阅者可以互相接触, 而发布订阅模式 发布者和订阅者互相不直接交流,中间有一层事件中心。 (实现起来的核心思想是一样的)

abap 复制代码
class Publisher { // 目标对象
  constructor() {
    this.observers = [] // 观察此对象的观察者们
  }
  add(observer) {
    this.observers.push(observer);
  }
  del(observer) {
    if (this.observers.includes(observer)) {
      this.observers.splice(this.observers.findIndex(item => item === observer), 1)
    }
  }
  notify() {
    this.observers.forEach((observer) => {
      observer.update(this);
    })
  }
}
class Observer { // 观察者
  update() {
    console.log('do something');
  }
}

事件总线 发布订阅模式

所有事件的发布/订阅操作,必须经由事件中心,禁止一切"私下交易"!

abap 复制代码
class EventEmitter {
  constructor() {
    this.handlers = new Map();
  }
  on(name, cb) {
    if (!this.handlers.has(name)) {
      this.handlers.set(name, []);
    }
    this.handlers.get(name).push(cb);
  }
  emit(name, ...args) {
    if (this.handlers.has(name)) {
      const cbs = this.handlers.get(name);
      cbs.forEach(cb => cb(...args))
    }
  }
  off(name, cb) {
    if (this.handlers.has(name)) {
      const cbs = this.handlers.get(name);
      const idx = cbs.findIndex(item => item === cb);
      cbs.splice(idx, 1)
    }
  }
}

vue 的响应式原理、watch

在 Vue 中,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新------这是一个典型的观察者模式。

每个响应式对象也相当一个 publisher,每次修改时会触发set()方法,通知所有的observer()更新。

  • publisher: vue组件, 响应式数据, watch方法的第一个入参
  • observer: watcher对象。watcher 接收到新的数据后,会去更新视图。

发布者实现 vue2

abap 复制代码
class Dep {
    constructor() {
        // 初始化订阅队列
        this.subs = []
    }
    
    // 增加订阅者
    addSub(sub) {
        this.subs.push(sub)
    }
    
    // 通知订阅者(是不是所有的代码都似曾相识?)
    notify() {
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}   

订阅者实现:

abap 复制代码
// observe方法遍历并包装对象属性
function observe(target) {
    // 若target是一个对象,则遍历它
    if(target && typeof target === 'object') {
        Object.keys(target).forEach((key)=> {
            // defineReactive方法会给目标属性装上"监听器"
            defineReactive(target, key, target[key])
        })
    }
}

// 定义defineReactive方法
function defineReactive(target, key, val) {
    // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
    observe(val)
    // 为当前属性安装监听器
    Object.defineProperty(target, key, {
         // 可枚举
        enumerable: true,
        // 不可配置
        configurable: false, 
        get: function () {
            return val;
        },
        // 监听器函数
        set: function (value) {
            console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
            val = value
        }
    });
}

迭代器模式

定义: 迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

比如说Array.prototype.forEach 只能在原型链在数组的聚合对象上使用,一些类数组就无法访问这个方法。

ES6中迭代器的实现

ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历------准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。

abap 复制代码
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()

因此,只要是配置了[Symbol.iterator] 的结构对象,都能使用for of 来循环。

相关推荐
技术人的流水账7 分钟前
我的Vide Coding工具的尝试——版本问题的苦之2
javascript
HashTang29 分钟前
买了专业屏只当普通屏用?解锁 BenQ RD280U 的“隐藏”开发者模式
前端·javascript·后端
мо仙堡杠把子ご灬30 分钟前
【无标题】
javascript
董世昌4136 分钟前
js遍历数组和对象的常用方法有哪些?
开发语言·javascript·ecmascript
小oo呆1 小时前
【学习心得】Python的Pydantic(简介)
前端·javascript·python
angelQ1 小时前
Vercel部署:前后端分离项目的整体部署流程及问题排查
前端·javascript
Jing_Rainbow1 小时前
【 前端三剑客-35 /Lesson58(2025-12-08)】JavaScript 原型继承与对象创建机制详解🧬
前端·javascript·面试
前端小L1 小时前
专题二:核心机制 —— reactive 与 effect
javascript·源码·vue3
如果你好1 小时前
# Vue 事件系统核心:createInvoker 函数深度解析
前端·javascript·vue.js
代码老祖1 小时前
vue3 vue-pdf-embed实现pdf自定义分页+关键词高亮
前端·javascript