背景
最近在研究react
的状态管理器zustand
时,研究源码时发现其组件注册绑定是通过观察者模式结合react hooks
实现更新的。而联想之前写vue
的时候,经常会用到vue
内置的自定义事件进行组件通信($emit
/on
),这个应该是发布订阅模式,搞得我有点头大,感觉这两种模式又十分相似,自己也是有点迷糊,感觉没有理解透,因此,这次就顺势深入研究下这两种模式,再尝试自己手写实现加深下理解。这篇文章是我个人的梳理心得,如有错误欢迎指正,共同进步~
对比
区别
观察者模式:在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变自动通知它们。
发布-订阅设计模式:消息的发送方(发布者)不会直接发送给特定的接收者(叫做订阅者),而是通过一个信息中介进行过滤和分配消息。
通俗形象点来说就是:
- 察者模式没中间商赚差价,发布订阅模式 有中间商赚差价。
- 观察者模式为一刀切模式,对所有订阅者一视同仁,发布订阅模式可以戴有色眼镜,有一层过滤或者说暗箱操作。
贴张图大家感受下
总结一下
-
在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
-
在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
-
观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
-
观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
概念看上去似乎也挺清晰的,它们之间的差异点等也比较好理解。接下来我们就开始自己动手实现,深入其内部原理和运行逻辑。
发布订阅模式
vue
自定义事件Event Bus
就是发布订阅模式的实现,还有Nodejs
的Emitter 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 实现
结构梳理
首先我们根据源码分析下结构,梳理一下vue
的event
实现逻辑
-
把事件中心
_events
挂载到Vue
实例上:vm._events = {}
-
把所有的方法:
$on
、$once
、$off
、$emit
挂载到Vue原型上
这样做的好处是可以在Vue组件中使用时直接
this.$on
、this.$emit
// $on
Vue.prototype.$on = function(){}
// $once
Vue.prototype.$once = function(){}
// $once
Vue.prototype.$off = function(){}
// $once
Vue.prototype.$emit = function(){}
看代码
-
$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 }
-
$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 }
-
$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 }
-
$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
的双向数据绑定就是观察者模式的实现。
利用 Object.defineProperty()
对数据进行劫持,设置一个监听器 Observer
,用来监听所有属性,如果属性上发上变化了,就需要告诉订阅者 Watcher
去更新数据,最后指令解析器 Compile
解析对应的指令,进而会执行对应的更新函数,从而更新视图,实现了双向绑定~
vue2.x 核心是通过
Object.defineProperty()
这个方法对数据劫持,并重新定义set
和get
方法,一旦数据变动,收到通知,更新视图。
Vue
在初始化时会有个依赖收集处理,通过对属性和指令的遍历处理(这里的属性包括props
、data
等,指令是通过compile
编译进行过滤处理得到),得到需要进行响应式处理的属性,然后再通过Observer
、Dep
、Watcher
实现监听、依赖收集、订阅。
有兴趣的朋友可以试着把源码下载下来,然后使用浏览器断点调试看一下 Vue
整个初始化的过程,对大家理解 Vue
的运行逻辑和过程还是很有帮助的。
这里我们简单看下 Vue
对 data
的处理。其他渲染过程暂不分析。
初始化 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
其实是把数组的原有方法进行了重写,比如push
,pop
,先执行原逻辑函数,如果是往数组新增元素,则把新增元素变成响应式。
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
的改写使得我们在修改数据的时候可以触发通知,这样便可以使所有添加订阅了的属性进行更新,然后再结合 Vue
的 compiler
编译进行 render
即可完成视图层的更新。
到这一步已经可以做到对数据的通知更新,但是我们都知道 vue
是双向数据绑定的,在数据变更的同时会继续通知视图也进行更新。即在模板编译器 complie
的时候会对指令(v-bind
、v-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
}
- 简单总结下
-
创建
store
拿到对外暴露唯一接口useStore
,定义全局状态。 -
通过
const bears = useStore(state => state.bears)
获取状态并与组件绑定。- 这一步
store
会执行subscribe(listener)
添加订阅操作,同时该方法内置有forceUpdate()
函数用于触发组件更新。
- 这一步
-
使用
set
钩子函数修改状态。- 即调用的
setState
方法,该方法会执行listeners.forEach((listener) => listener(state, previousState))
通知所有订阅者执行更新。
- 即调用的
结语
观察者模式和发布订阅模式在实际项目中非常常见,很多优秀的第三方库也是借鉴了这两种设计模式的思想 —— 比如 Vue
,Vue Event
,React Event
,RxJS
,Redux
,zustand
等。
对于项目中一些逻辑的解耦或者解决一些异步的问题非常有帮助。可以毫不夸张的说:发布订阅模式/观察者模式可以解决大部分解耦问题。
总的来说,阅读学习一些优秀的库(包括一些它们内部封装的工具函数,有很多巧妙的设计和实现),这种源码学习对于我们自身成长与技术拓展是很有帮助的,很多时候我们会被大佬们的独特思路和设计所折服,而通过进一步理解掌握,我们完全可以吸收自用,在日后的实战项目中大显身手,岂不美哉!😎