速通 JavaScript 代理模式和发布订阅模式

开发 前端
发布订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。

1. 前言

JavaScript 是一门动态语言,在实现设计模式的时候,往往会比 Java 等静态语言更简便,本文将介绍在 JavaScript 中如何实现代理模式和发布订阅模式。

2. 代理模式

2.1. 定义

在介绍定义时还是以类图为主,虽然 JavaScript 实现设计模式时可能不会使用到类,但是类图提供了一种通用的设计模式实现思想。

代理模式定义:为其他对象提供一种代理以控制对这个对象的访问。

其类图如下:

图片图片

类图中的三个角色:

  • Subject 抽象主题角色:定义了具体主题和代理主题的共同接口,这样在任何使用具体主题的地方都可以使用代理主题。
  • RealSubject 具体主题角色:逻辑的具体执行者。
  • Proxy 代理主题角色:实现了抽象主题接口,并持有对具体主题的引用。

2.2. 实现

在 JavaScript 中,你可以使用 Proxy 轻松实现代理模式,比如可以通过代理模式实现一个只接收 number 类型值的数组。

const arr = []
const numArr = new Proxy(arr, {
  set(target, key, value, proxy) {
    if (typeof value !== 'number') {
      throw Error("属性只能是 number 类型");
    }
    return Reflect.set(target, key, value, proxy);
  }
})
numArr.push(0)
numArr.push('1') // Uncaught Error: 属性只能是 number 类型
console.log(numArr) // Proxy(Array) {0: 0}

利用 Proxy,你还可以实现响应式编程。

const data = { userName: '' }

const render = (info) => {
  console.log(info)
  // 根据数据渲染界面
}

const proxyData = new Proxy(data, {
  set(target, key, value, receiver) {
    // 设置值
    Reflect.set(target, key, value, receiver)
    // 重新触发渲染
    render(target)
  }
})

data.userName = 'xiaoming'// 控制台输出 { userName: 'xiaoming' }

当然你也可以利用 Proxy 来实现日志功能,用于跟踪函数调用情况。

function add(a, b) {
  return a + b;
}

// 日志记录函数
function log(message) {
  console.log(message);
}

// 创建代理对象
const proxy = newProxy(add, {
// 拦截函数调用
  apply(target, thisArg, args) {
    const result = Reflect.apply(target, thisArg, args);
    log(`函数 ${target.name} 被调用,参数: [${args.join(', ')}],返回值: ${result}`);
    return result;
  }
});

const sum1 = proxy(1, 2);  // 输出: 函数 add 被调用,参数: [1, 2],返回值: 3
const sum2 = proxy(3, 4);  // 输出: 函数 add 被调用,参数: [3, 4],返回值: 7

2.3. 小结

最后提一下代理模式和装饰模式的异同点,两者的共同点是代理类或装饰类和原本类都具有相同的接口,不同点则是代理模式着重对代理过程的控制,而装饰模式则是对类的功能进行加强或减弱。

3. 发布订阅模式

3.1. 定义

发布订阅模式的定义:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

其类图如下:

图片图片

发布订阅模式经常会和观察者模式做对比,两个设计模式广义上设计理念是一致的,在实现上有些差别,本文更注重实际应用,故不展开此内容,借用一张图来说明。

图片图片

3.2. 实现

接着来完成发布订阅模式的简单实现,主要是实现 subscribe 和 publish 方法。

const event = { 
  listeners: [],  // 所有订阅者集合
  // 订阅函数
  subscribe: function(fn) { 
    this.listeners.push(fn)
  },
  // 发布函数
  publish: function() {
    for(let i = 0; i < this.listeners.length; i++) { 
      this.listeners[i]()
    } 
  },
  // 移除订阅函数
  unsubcribe: function(fn) { 
    const fns = this.listeners;
    // 倒序访问方便使用 splice 移除订阅函数
    for (let l = fns.length - 1; l >=0; l--) {
      const _fn = fns[l]; 
      if (_fn === fn){ 
        fns.splice(l, 1);
      } 
    } 
  }
}

const fn1 = () => { console.log('trigger1') }
const fn2 = () => { console.log('trigger2') }
event.subscribe(fn1)
event.subscribe(fn2)
event.publish() // 控制台打印 trigger1, trigger2
event.unsubcribe(fn1)
event.publish() // 控制台打印 trigger2

到此我们实现了一个简单版本的发布订阅。

接下来我们基于发布订阅模式,在 React 中实现一个类似 Zustand 的状态管理功能。

首先我们需要了解一个 React 官方 Hook useSyncExternalStore,这个 Hook 可以让你订阅一个外部数据源,当其中数据发生变化时,React 会触发重新渲染。

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

可以看到该 Hook 参数有三个,这里我们主要关注前两个,第一个参数即订阅函数,第二个参数为获取数据源的函数,第三个和服务端渲染相关。

接下来我们要结合发布订阅模式和 useSyncExternalStore 实现一个简单版本的 Zustand。

const createImpl = (createState) => {
  // 相比发布订阅模式,多了个状态值
  let state
  let initialState
  const listeners = newSet()

  // 类似发布订阅模式中的 publish 方法,最终会触发订阅者
  const setState = (nextState) => {    
    // 对比状态值是否有变化
    if (!Object.is(nextState, state)) {
      const previousState = state
      state = Object.assign({}, state, nextState)
      // 触发订阅函数
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  const getState = () => state

  const getInitialState = () => initialState

  const subscribe = (listener) => {
    listeners.add(listener)
    // 返回一个取消订阅的方法
    return() => {
      listeners.delete(listener)
    }
  }

  // 清空订阅
  const destory = () => listeners.clear()

  const api = {
    setState,
    getState,
    getInitialState,
    subscribe,
    destory
  }

  // 调用 createState 方法返回初始状态值,createState 参数为 set、get 和 api 对象
  initialState = (state = createState(setState, getState, api))

  return api
}

const create = (createState) => {
  const api = createImpl(createState)
  // 传入订阅方法和获取数据方法到 useSyncExternalStore
  const useStore = () => useSyncExternalStore(api.subscribe, api.getState)
  // 把 api 合并到 Hook 上
  Object.assign(useStore, api)
  return useStore
}

exportdefault create

先来看下 createImpl 函数,相比于我们实现的简单版发布订阅模式,createImpl 内部多维护了一个状态值,在调用发布方法(setState)时,会更新状态值,并触发订阅函数,订阅函数入参为新旧状态值。

最后我们看下如何使用自己的状态管理功能。

// create 方法接收一个函数参数,内部会调用函数初始化状态值,最终返回一个 Hook
const useStore = create((set) => ({
  num: 1,
  // 通过 set 方法更新状态值,更新后触发所有订阅函数的调用
  random: () =>set({ num: Math.round(Math.random() * 1000) }),
}))

function Counter() {
  // 调用 useStore,useStore 会调用 React useSyncExternalStore
  const { num, random } = useStore();
  return (
    <div>
      <p>{`Number: ${num}`}</p>
      <button onClick={random}>Random</button>
    </div>
  )
}

create 方法接收一个函数参数,用于初始化状态,最终 create 会返回一个 Hook。在状态值中, random 方法会调用发布方法(setState)触发更新,因为 useSyncExternalStore 会使用第一个参数完成订阅动作,所以此时它能接收到数据更新,随后便返回最新的状态值,并触发重新渲染。

在线代码示例:https://stackblitz.com/edit/react-9nvjhwhx?file=demo.tsx

至此我们实现了一个简单版本 Zustand。

3.3. 小结

发布订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。

3. 总结

设计模式大体思想是要把系统中不变和变化的部分分开,封装不变的部分,根据业务灵活替换变化的部分,这样就可以保证系统的健壮性和可拓展性。同时在实现设计模式的同时,你通常也会很好的遵守了设计模式原则,如单一职责、依赖倒置、开闭原则、迪米特原则等。

责任编辑:武晓燕 来源: 栗子前端
相关推荐

2012-02-29 09:41:14

JavaScript

2022-06-27 13:56:10

设计模式缓存分布式系统

2023-11-10 09:22:06

2022-12-02 07:28:58

Event订阅模式Spring

2009-11-05 10:07:37

WCF设计模式

2021-06-29 08:54:23

设计模式代理模式远程代理

2012-01-13 15:59:07

2021-09-08 07:18:30

代理模式对象

2010-03-25 08:52:30

PHP设计模式代理模式

2011-04-06 11:41:25

Java动态代理

2024-02-26 11:52:38

代理模式设计

2022-11-30 17:05:33

代码程序场景

2021-08-02 17:21:08

设计模式订阅

2024-07-29 08:34:18

C++订阅者模式线程

2015-09-08 13:39:10

JavaScript设计模式

2024-04-10 12:27:43

Python设计模式开发

2022-09-07 08:25:08

代理模式设计模式代码

2023-12-04 08:24:23

2023-11-02 21:11:11

JavaScript设计模式

2011-03-23 10:40:51

java代理模式
点赞
收藏

51CTO技术栈公众号