当设计模式遇上前端

创建模式

单例模式

证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。 很好理解,比如说一个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 来循环。

相关推荐
Jiaberrr20 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ2 小时前
html+css+js实现step进度条效果
javascript·css·html
john_hjy3 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd3 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
yanlele3 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo4 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
xgq4 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
李是啥也不会4 小时前
数组的概念
javascript