阅读 260
发布订阅模式vs观察者模式

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

发布订阅模式vs观察者模式 | 小帅の技术博客 (ssscode.com)

背景

最近在研究react的状态管理器zustand时,研究源码时发现其组件注册绑定是通过观察者模式结合react hooks实现更新的。而联想之前写vue的时候,经常会用到vue内置的自定义事件进行组件通信($emit/on),这个应该是发布订阅模式,搞得我有点头大,感觉这两种模式又十分相似,自己也是有点迷糊,感觉没有理解透,因此,这次就顺势深入研究下这两种模式,再尝试自己手写实现加深下理解。这篇文章是我个人的梳理心得,如有错误欢迎指正,共同进步~

对比

区别

观察者模式:在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变自动通知它们。

发布-订阅设计模式:消息的发送方(发布者)不会直接发送给特定的接收者(叫做订阅者),而是通过一个信息中介进行过滤和分配消息。

通俗形象点来说就是:

  • 察者模式没中间商赚差价,发布订阅模式 有中间商赚差价。
  • 观察者模式为一刀切模式,对所有订阅者一视同仁,发布订阅模式可以戴有色眼镜,有一层过滤或者说暗箱操作。

贴张图大家感受下

15fe1b1f174cd376.webp

总结一下

  • 观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。

  • 发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。

  • 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。

  • 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

概念看上去似乎也挺清晰的,它们之间的差异点等也比较好理解。接下来我们就开始自己动手实现,深入其内部原理和运行逻辑。

发布订阅模式

vue自定义事件Event Bus就是发布订阅模式的实现,还有NodejsEmitter Event

实现一个支持订阅、解绑、发布、同类型事件支持多次绑定的发布订阅。

来个简单实现

上代码

// 订阅中心
const subscribers = {}
// 订阅
const subscribe = (type, fn) => {
  // 以数组模式添加队列,做到同一类型支持多个绑定
  if (!subscribers[type]) subscribers[type] = []
  subscribers[type].push(fn)
}
// 发布
const publish = (type, ...args) => {
  if (!subscribers[type] || !subscribers[type].length) return
  subscribers[type].forEach((fn) => fn(...args))
}
// 解绑订阅
const unsubscribe = (type, fn) => {
  if (!subscribers[type] || !subscribers[type].length) return
  subscribers[type] = subscribers[type].filter((n) => n !== fn)
}
复制代码

验证测试

// console test ======>
subscribe("topic-1", () => console.log("suber-A 订阅了 topic-1"))
subscribe("topic-2", () => console.log("suber-B 订阅了 topic-2"))
subscribe("topic-1", () => console.log("suber-C 订阅了 topic-1"))

publish("topic-1") // 通知订阅了 topic-1 的 A 和 C

// 输出结果
// suber-A 订阅了 topic-1
// suber-C 订阅了 topic-1
复制代码

实现一个Emitter类

上代码

class Emitter {
  constructor() {
    // 订阅中心
    this._event = this._event || {}
  }
  // 注册订阅
  addEventListener(type, fn) {
    const handler = this._event[type]

    if (!handler) {
      this._event[type] = [fn]
    } else {
      handler.push(fn)
    }
  }
  // 卸载订阅
  removeEventListener(type, fn) {
    const handler = this._event[type]

    if (handler && handler.length) {
      this._event[type] = handler.filter((n) => n !== fn)
    }
  }
  // 通知
  emit(type, ...args) {
    const handler = this._event[type]

    if (handler && handler.length) {
      handler.forEach((fn) => fn.apply(this, args))
    }
  }
}
复制代码

验证测试

// console test ======>
const emitter = new Emitter()

emitter.addEventListener("change", (obj) => console.log(`name is ${obj.name}`))

emitter.addEventListener("change", (obj) => console.log(`age is ${obj.age}`))

const sex = (obj) => console.log(`sex is ${obj.sex}`)

emitter.addEventListener("change", sex)

emitter.emit("change", { name: "xiaoming", age: 28, sex: "male" })

console.log("event-A", emitter._event)

emitter.removeEventListener("change", sex)

console.log("====>>>>")

emitter.emit("change", { name: "xiaoming", age: 28, sex: "male" })

console.log("event-B", emitter._event)

// 输出
// name is xiaoming
// age is 28
// sex is male
// event-A {change: Array(3)}

// ====>>>>

// name is xiaoming
// age is 28
// event-B {change: Array(2)}
复制代码

vue Event Bus 实现

结构梳理

源码位置:src/core/instance/events.js

首先我们根据源码分析下结构,梳理一下vueevent实现逻辑

  1. 把事件中心 _events 挂载到 Vue 实例上:

    vm._events = {}

  2. 把所有的方法: $on$once$off$emit 挂载到Vue原型上

这样做的好处是可以在Vue组件中使用时直接 this.$onthis.$emit

// $on
Vue.prototype.$on = function(){}
// $once
Vue.prototype.$once = function(){}
// $once
Vue.prototype.$off = function(){}
// $once
Vue.prototype.$emit = function(){}
复制代码

看代码

  1. $on 添加注册

    // $on
    Vue.prototype.$on = function (event, fn) {
      const vm = this
    
      // 如果传入的 event 监听事件类型为数组,递归调用 $on 方法
      if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
          vm.$on(event[i], fn)
        }
      } else {
    
        // 如果存在直接添加,不存在新建后添加
        ;(vm._events[event] || (vm._events[event] = [])).push(fn)
      }
    
      // 返回this,用于链式调用
      return vm
    }
    复制代码
  2. $once 单次执行

    // $once
    Vue.prototype.$once = function (event, fn) {
      const vm = this
    
      // 当该 event 事件触发时,调用 on 方法
      function on() {
    
        // 首先执行 $off 方法卸载 本回调方法
        vm.$off(event, on)
    
        // 再执行 本回调方法
        fn.apply(vm, arguments)
      }
    
      // 该赋值会在 $off 中使用:cb.fn === fn
      // 因为该 $once 方法调用的是 $on 添加回调,但是添加的是包装后的 on 方法而不是 fn 方法
      // 因此当我们单独调用 $off方法删除 fn 回调时,是找不到的,这时就可以通过 cb.fn === fn 判断
      on.fn = fn
    
      // 调用 $on 方法,把该回调添加到队列
      vm.$on(event, on)
    
      return vm
    }
    复制代码
  3. $off 卸载删除

    // $off
    Vue.prototype.$off = function (event, fn) {
      const vm = this
    
      // 如果不传入任何参数,清空所有的事件
      if (!arguments.length) {
        vm._events = Object.create(null)
        return vm
      }
    
      // 如果 event 为数组,同 $on 逻辑,递归卸载事件
      if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
          vm.$off(event[i], fn)
        }
        return vm
      }
    
      // 回调列表
      const cbs = vm._events[event]
    
      // 如果该 event 事件不存在绑定回调,不处理
      if (!cbs) {
        return vm
      }
    
      // 如果未传入对应 event 的解绑回调,则清空该 event 的所有
      if (!fn) {
        vm._events[event] = null
        return vm
      }
    
      // event 事件类型和 回调 都存在,遍历查找删除 指定 回调
      let cb
      let i = cbs.length
      while (i--) {
        cb = cbs[i]
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1)
          break
        }
      }
      return vm
    }
    复制代码
  4. $emit 触发事件

    // $emit
    Vue.prototype.$emit = function (event) {
      const vm = this
    
      // 回调列表
      let cbs = vm._events[event]
    
      // 判断该 event 是否存在执行回调
      if (cbs) {
    
        // $emit方法可以传参,这些参数会在调用回调函数的时候传进去
        // 排除 event 参数的其他参数
        // toArray 是一个把类数组转换为数组的方法,并支持截取
        const args = toArray(arguments, 1)
    
        // 遍历回调函数
        for (let i = 0, l = cbs.length; i < l; i++) {
          cbs[i].apply(vm, args)
        }
      }
      return vm
    }
    复制代码
    toArray方法
    // Convert an Array-like object to a real Array.
    function toArray (list, start) {
      start = start || 0;
      var i = list.length - start;
      var ret = new Array(i);
      while (i--) {
        ret[i] = list[i + start];
      }
      return ret
    }
    复制代码

测试下

我们先模拟一个Vue类测试下

class Vue {
  constructor() {
    this._events = {}
  }

  // 提供一个对外获取 _events 的接口
  get event() {
    return this._events
  }
}
复制代码

验证下结果

// 实例化
const myVue = new Vue()

// 添加订阅
const update_user = (args) => console.log("user:", args)
const once_update_user = (args) => console.log("once_user:", args)

myVue.$on("user", update_user)
myVue.$once("user", once_update_user) // 该订阅触发后自动卸载

// 输出打印
console.log("events:", myVue.event)
// events: {user: [(args) => console.log("user:", args), ƒ on()]}

// 触发通知
myVue.$emit("user", { name: "xiaoming", age: 18 })
console.log("events:", myVue.event)
// events: {user: [(args) => console.log("user:", args)]}
// user: {name: "xiaoming", age: 18}
// once_user: {name: "xiaoming", age: 18}

// 卸载订阅
myVue.$off("user", once_update_user)
console.log("events:", myVue.event)
// events:{user: []}
复制代码

小总结下

Vue 封装的这个发布订阅模式,可以说是很完善了,这个是完全可以独立抽取出来的在其他项目中使用的代码,再根据自身需求,调整下事件存储器的位置即可(Vue 放在了实例上)。

我们从最简单的几行代码,一直到框架中的细致完整实现,从中可以发现:其实只要我们思路对了,核心方法掌握理解了,很容易就可以弄明白其实现原理,而剩下的大多都是对各种异常情况的判断和处理。

观察者模式

只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

也来个简单实现

// 观察者列表
const observers = []

// 添加
const addob = (ober) => {
  observers.push(ober)
}

// 通知
const notify = (...args) => {
  observers.forEach((fn) => fn(args))
}

// 测试 =======>
const subA = () => console.log("I am sub A")
const subB = (args) => console.log("I am sub B", args)

addob(subA)
addob(subB)
notify({ name: "sss", site: "ssscode.com" })
// I am sub A
// I am sub B [{name: "sss", site: "ssscode.com"}]
复制代码

实现一个观察者类

上代码

// 观察者
class Observer {
  constructor(name) {
    // 观察者 name
    this.name = name
  }

  // 触发器
  update() {
    console.log("观察者:", this.name)
  }
}

// 被观察者
class Subject {
  constructor() {
    // 观察者列表
    this._observers = []
  }

  // 获取 观察者列表
  get obsers() {
    return this._observers
  }

  // 添加
  add(obser) {
    this._observers.push(obser)
  }

  // 移除
  remove(obser) {
    this._observers = this._observers.filter((n) => n !== obser)
  }

  // 通知所有观察者
  notify() {
    this._observers.forEach((obser) => obser.update())
  }
}
复制代码

验证测试下结果

// 观察者
const obserA = new Observer("obser-A")
const obserB = new Observer("obser-B")

// 被观察者
const subject = new Subject()

// 添加到 观察者列表
subject.add(obserA)
subject.add(obserB)

// 通知
subject.notify()
console.log("观察者列表:", subject.obsers)
// 观察者: obser-A
// 观察者: obser-B
// 观察者列表: (2) [Observer, Observer]

// 移除
subject.remove(obserA)

// 通知
subject.notify()
console.log("观察者列表:", subject.obsers)
// 观察者: obser-B
// 观察者列表: [Observer]
复制代码

Vue 双向数据绑定

vue的双向数据绑定就是观察者模式的实现。

166a031209fc8da5.webp

利用 Object.defineProperty() 对数据进行劫持,设置一个监听器 Observer,用来监听所有属性,如果属性上发上变化了,就需要告诉订阅者 Watcher 去更新数据,最后指令解析器 Compile 解析对应的指令,进而会执行对应的更新函数,从而更新视图,实现了双向绑定~

vue2.x 核心是通过 Object.defineProperty() 这个方法对数据劫持,并重新定义 setget 方法,一旦数据变动,收到通知,更新视图。

Vue在初始化时会有个依赖收集处理,通过对属性和指令的遍历处理(这里的属性包括 propsdata 等,指令是通过 compile 编译进行过滤处理得到),得到需要进行响应式处理的属性,然后再通过 ObserverDepWatcher 实现监听、依赖收集、订阅。

有兴趣的朋友可以试着把源码下载下来,然后使用浏览器断点调试看一下 Vue 整个初始化的过程,对大家理解 Vue 的运行逻辑和过程还是很有帮助的。

这里我们简单看下 Vuedata 的处理。其他渲染过程暂不分析。

初始化 initData

// 这里的 $options 其实就是我们在写 Vue 的时候
// 其中的props、data、method、computed等属性。
function initData(vm) {
  // 对 data 进行处理,函数 / 对象
  let data = vm.$options.data
  // 为什么建议大家vue中的data使用函数式写法?
  // 当一个组件被定义, data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例
  // 如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象
  // 通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,
  // 从而返回初始数据的一个全新副本数据对象。
  // (js在赋值object对象时,是直接一个相同的内存地址。所以为了每个组件的data独立,采用了这种方式。)
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}

  // observe data
  observe(data, true /* asRootData */)
}
复制代码

创建观察者 observe

function observe(value, asRootData) {
  let ob
  // Observer
  ob = new Observer(value)
  // asRootData = true
  if (asRootData && ob) {
    ob.vmCount++
  }
  //
  return ob
}
复制代码

观察者类 Observer

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    if (Array.isArray(value)) {
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  // 处理所有属性,进行响应式处理
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // 数组 时遍历处理
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

数据劫持,包装 set 方法,监听数据更新 defineReactive

由于 Object.defineProperty 不能够监听数组下标,所以这里 Vue 其实是把数组的原有方法进行了重写,比如pushpop,先执行原逻辑函数,如果是往数组新增元素,则把新增元素变成响应式。

function defineReactive(obj, key, val) {
  // 依赖收集
  const dep = new Dep()

  // 数据劫持,包装 set 方法添加 notify 通知
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 如果 watcher 存在,触发依赖收集
      if (Dep.target) {
        dep.depend()
      }

      return val
    },
    set: function reactiveSetter(newVal) {
      // ...
      // 数据变更 ==> 触发set方法 ==> 调用dep.notify()通知更新
      dep.notify()
    },
  })
}
复制代码

依赖收集类 Dep

class Dep {
  constructor() {
    this.id = uid++
    this.subs = [] // 用于存放订阅者 Watcher
  }

  addSub(sub) {
    // sub ===> Watcher
    // 该方法会在 watcher 添加订阅时被执行
    this.subs.push(sub)
  }

  removeSub(sub) {
    // sub ===> Watcher
    remove(this.subs, sub)
  }

  // Dep.target===watcher 即 watcher.addDep
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    // subs
    const subs = this.subs.slice()
    // 调用 watcher 的 update
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// 存放唯一 watcher
Dep.target = null
const targetStack = []

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
复制代码

订阅者 watcher

// 删减了部分,只看核心代码
class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    vm._watchers.push(this)

    this.cb = cb
    this.deps = []
    this.newDeps = []
    this.value = this.get()
    this.getter = expOrFn
  }

  // 获取最新 value, 收集依赖
  get() {
    pushTarget(this)

    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)

    if (this.deep) {
      // 收集嵌套属性的每个依赖
      traverse(value)
    }

    popTarget()
    this.cleanupDeps()

    return value
  }

  // 添加依赖
  // dep === class Dep
  addDep(dep) {
    this.newDeps.push(dep)
    dep.addSub(this)
  }

  // 清除依赖
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      dep.removeSub(this)
    }
    this.deps = this.newDeps
  }

  // 提供更新的接口
  update() {
    this.run()
  }

  // 通知执行更新
  run() {
    const value = this.get()
    this.cb.call(this.vm, value, oldValue)
  }

  // 通过 watcher 收集所有依赖
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
复制代码

通过上面代码我们可以发现,Vue 在初始化的时候对整个 data 对象中的每个属性都进行了添加订阅监听处理,而通过对 set 的改写使得我们在修改数据的时候可以触发通知,这样便可以使所有添加订阅了的属性进行更新,然后再结合 Vuecompiler 编译进行 render 即可完成视图层的更新。

到这一步已经可以做到对数据的通知更新,但是我们都知道 vue 是双向数据绑定的,在数据变更的同时会继续通知视图也进行更新。即在模板编译器 complie 的时候会对指令(v-bindv-modle等)进行过滤并添加 Watcher 订阅,实现 observe <===> watcher <===> complie 三者之间的绑定与通信 。

watcher 源码: github1s.com/vuejs/vue/b…

这里我就不继续深入了,感觉有点说不完了🤣,真让人头大,涉及的内容有点多,有机会的话搞搞阅读源码系列了。。。,有点扯远了,回到文章这里我们是主要抛出观察者模式的使用场景,以及对 Vue 的初始化过程与双向绑定原理探讨,收~

有兴趣的同学可以看看这篇文章:观察者模式实现vue双向数据绑定

zustand 状态管理器

先来看看用法

创建 store

// store
import create from 'zustand'

// 通过 create 方法创建一个具有响应式的 store
const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })), // 函数写法
  removeAllBears: () => set({ bears: 0 }) // 对象写法
}))
复制代码

组件引用

// UI 组件,展示 bears 状态,当状态变更时可实现组件同步更新
function BearCounter() {
  const bears = useStore(state => state.bears)
  return <h1>{bears} around here ...</h1>
}

// 控制组件,通过 store 内部创建的 increasePopulation 方法执行点击事件,可触发数据和UI组件更新
function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}
复制代码

结合官方示例,可以确定 zustand 内部对通过 state 绑定的组件,默认添加注册到了订阅者队列,此时该 bears 属性相当于一个被观察者,当 bears 状态变更后,通知所有订阅了该数属性的组件进行更新。(我们可以大致推测一下这个 set 方法)

废话不多说,看代码,我们先按照创建 store 的逻辑分析:

create 接受一个函数(非函数情况暂时不研究),返回我们定义的状态和方法,且该函数是提供 set 方法供我们使用的,而这个 set 方法必定是可以触发更新通知的。

直接上代码

  • create 方法
function create(createState) {
  // 初始化处理 createState
  const api = typeof createState === "function" ? createImpl(createState) : createState
}
复制代码
  • 这里引入了一个 createImpl 方法,我们先看下这个方法对 createState 的处理和返回值。
function createImpl(createState) {
  // 用于缓存上一次的 状态
  let state
  // 监听队列
  const listeners = new Set()

  const setState = (partial, replace) => {
    // 如果是 function 注入 state 并获取执行结果,否则直接取值
    // 例如:setCount: ()=> set(state=> ({state: state.count +1 })
    // 例如:setCount: ()=> set({count: 10})
    const nextState = typeof partial === "function" ? partial(state) : partial
    // 优化:判断状态是否变化了,再更新组件状态
    if (nextState !== state) {
      // 上一次状态
      const previousState = state
      // 当前状态最新状态
      state = replace ? nextState : Object.assign({}, state, nextState)
      // 通知队列中的每一个组件
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  // 函数获取 state
  const getState = () => state

  // 存在 selector 或 equalityFn 参数时,对订阅方法进行处理
  const subscribeWithSelector = (listener, selector = getState, equalityFn = Object.is) => {
    // 当前拿到的值
    let currentSlice = selector(state)
    // 实际添加到队列的是 listenerToAdd 方法,
    function listenerToAdd() {
      // 订阅通知执行时的值,即 下一次更新的值
      const nextSlice = selector(state)
      // 对比前后值不相等,则触发更新通知
      if (!equalityFn(currentSlice, nextSlice)) {
        // 上一次值
        const previousSlice = currentSlice
        // 执行添加的订阅函数
        // 例如:useStore.subscribe(console.log, state => state.paw)
        // 中的 console.log
        listener((currentSlice = nextSlice), previousSlice)
      }
    }
    // add listenerToAdd
    listeners.add(listenerToAdd)
    // Unsubscribe
    return () => listeners.delete(listenerToAdd)
  }

  // 添加订阅 
  // 列如:useStore.subscribe(console.log, state => state.paw)
  // 效果:只监听 paw 的变化,通知更新
  const subscribe = (listener, selector, equalityFn) => {
    // selector 或 equalityFn 参数存在,走该逻辑,添加指定的订阅通知
    if (selector || equalityFn) {
      return subscribeWithSelector(listener, selector, equalityFn)
    }
    // 否则 对所有变更添加订阅通知
    listeners.add(listener)
    // Unsubscribe
    // 执行结果为删除该订阅者函数
    // 即:const unsubscribe= subscribe() = () => listeners.delete(listener)
    return () => listeners.delete(listener)
  }

  // 清除 订阅
  const destroy = () => listeners.clear()
  // 返回给 create 方法的处理结果,即返回了 4 个处理方法
  const api = { setState, getState, subscribe, destroy }
  // 其对传入的 createState 函数注入了3个参数 setState, getState, api 
  // 使得在 create 创建 store时,可以在回调函数的参数里取用方法对数据进行处理
  // 如:create(set=> ({count: 0,setCount: ()=> set(state=> ({state: state.count +1 }))}))
  // 并调用然后返回 api = { setState, getState, subscribe, destroy } 属性方法
  state = createState(setState, getState, api)

  return api
}
复制代码

可以得到 createImpl 的执行结果

const api = { setState, getState, subscribe, destroy }

然后我们再回来继续往下分析 create 方法

  • 简单介绍下代码中使用的 useEffect / useLayoutEffect 区别

    • useEffect 是异步执行的,而 useLayoutEffect 是同步执行的。
    • useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount 等价。
  • create 方法

import { useReducer, useLayoutEffect, useRef } from "react"

// 是否为非浏览器环境
const isSSR =
  typeof window === "undefined" ||
  !window.navigator ||
  /ServerSideRendering|^Deno\//.test(window.navigator.userAgent)

// useEffect 可以在服务端(NodeJs)执行,而 useLayoutEffect 不行
const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect

export default function create(createState) {
  const api = typeof createState === "function" ? createImpl(createState) : createState

  // 返回 useStore 函数供外部使用
  // 闭包使得 api 作为执行上下文,供 useStore 内部使用,保证数据隔离
  const useStore = (selector, equalityFn = Object.is) => {
    // 用于触发组件更新
    const [, forceUpdate] = useReducer((c) => c + 1, 0)

    // 获取 state
    const state = api.getState()
    // 把 state 挂载到 useRef,避免副作用对其进行影响而更新
    const stateRef = useRef(state)
    // 挂载指定 selector 方法到 useRef
    // 列如:const bears = useStore(state => state.bears)
    const selectorRef = useRef(selector)
    // 等值方法
    const equalityFnRef = useRef(equalityFn)
    // 标记错误
    const erroredRef = useRef(false)

    // 当前 state 属性(state.bears)
    const currentSliceRef = useRef()
    // 空值处理
    if (currentSliceRef.current === undefined) {
      currentSliceRef.current = selector(state)
    }

    let newStateSlice
    let hasNewStateSlice = false

    // The selector or equalityFn need to be called during the render phase if
    // they change. We also want legitimate errors to be visible so we re-run
    // them if they errored in the subscriber.
    if (
      stateRef.current !== state ||
      selectorRef.current !== selector ||
      equalityFnRef.current !== equalityFn ||
      erroredRef.current
    ) {
      // Using local variables to avoid mutations in the render phase.
      newStateSlice = selector(state)
      // 新旧值是否相等
      hasNewStateSlice = !equalityFn(currentSliceRef.current, newStateSlice)
    }

    // Syncing changes in useEffect.
    useIsomorphicLayoutEffect(() => {
      if (hasNewStateSlice) {
        currentSliceRef.current = newStateSlice
      }
      stateRef.current = state
      selectorRef.current = selector
      equalityFnRef.current = equalityFn
      erroredRef.current = false
    })

    // 暂存 state
    const stateBeforeSubscriptionRef = useRef(state)
    // 初始化
    useIsomorphicLayoutEffect(() => {
      const listener = () => {
        try {
          // 触发更新时的最新获取 state
          const nextState = api.getState()
          // 注入 nextState 执行传入的 selector 方法,获取值,即 state.bears
          const nextStateSlice = selectorRef.current(nextState)
          // 对比不相等 ==> 更新
          if (!equalityFnRef.current(currentSliceRef.current, nextStateSlice)) {
            // 更新 stateRef 为最新 state
            stateRef.current = nextState
            // 更新 currentSliceRef 为最新属性值,即 state.bears
            currentSliceRef.current = nextStateSlice
            // 更新组件
            forceUpdate()
          }
        } catch (error) {
          // 登记错误
          erroredRef.current = true
          // 更新组件
          forceUpdate()
        }
      }
      // 添加 listener 订阅
      const unsubscribe = api.subscribe(listener)
      // state已经变更,通知更新
      if (api.getState() !== stateBeforeSubscriptionRef.current) {
        listener() // state has changed before subscription
      }
      // 卸载时 清除订阅
      return unsubscribe
    }, [])

    return hasNewStateSlice ? newStateSlice : currentSliceRef.current
  }

  // 合并 api 属性到 useStore
  Object.assign(useStore, api)

  // 闭包暴露唯一 方法供外部使用
  return useStore
}
复制代码
  • 简单总结下
  1. 创建 store 拿到对外暴露唯一接口 useStore ,定义全局状态。

  2. 通过 const bears = useStore(state => state.bears) 获取状态并与组件绑定。

    • 这一步 store 会执行 subscribe(listener) 添加订阅操作,同时该方法内置有 forceUpdate() 函数用于触发组件更新。
  3. 使用 set 钩子函数修改状态。

    • 即调用的 setState 方法,该方法会执行 listeners.forEach((listener) => listener(state, previousState)) 通知所有订阅者执行更新。

结语

观察者模式和发布订阅模式在实际项目中非常常见,很多优秀的第三方库也是借鉴了这两种设计模式的思想 —— 比如 VueVue EventReact EventRxJSReduxzustand 等。

对于项目中一些逻辑的解耦或者解决一些异步的问题非常有帮助。可以毫不夸张的说:发布订阅模式/观察者模式可以解决大部分解耦问题。

总的来说,阅读学习一些优秀的库(包括一些它们内部封装的工具函数,有很多巧妙的设计和实现),这种源码学习对于我们自身成长与技术拓展是很有帮助的,很多时候我们会被大佬们的独特思路和设计所折服,而通过进一步理解掌握,我们完全可以吸收自用,在日后的实战项目中大显身手,岂不美哉!😎

e18d20c94006dfe0-20cbe3c7627c7e45-ecac4f401c30655e59ef513cdd563397.jpg

用例

github.com/JS-banana/s…

参考

文章分类
前端
文章标签