React useSyncExternalStore:基于发布订阅模式的状态订阅机制
基于 v19.2.0 版本
在阅读 zustand 源码时,发现 zustand 使用了 React.useSyncExternalStore 进行实现。所以在深入阅读 zustand 前,
应先了解 useSyncExternalStore 的使用方法和实现逻辑。
React 官方文档中说明 useSyncExternalStore 是一个可以订阅外部 store 的 React Hook。它的使用场景包括: 1. 订阅外部 store 2. 订阅浏览器 API 3. 把逻辑提取到自定义 Hook 4. 添加服务端渲染支持
本文主要实践和分析订阅外部 store。
订阅外部状态,说明可以共享状态。两个组件共享状态,除了状态提升外,还可以借助发布订阅模式来实现。通过 useSyncExternalStore 的函数签名可以看出,它也借助了发布订阅模式的理念进行实现。
// todo store
let todoList = [{id: 1, text: 'Todo 1'}, {id: 2, text: 'Todo 2'}, {id: 3, text: 'Todo 3'}]
let listeners = []
const emitChange = () => {
listeners.forEach(listener => listener())
}
const todoStore = {
addTodo: (todo) => {
todoList = [...todoList, todo]
emitChange()
},
subscribe: (listener) => {
listeners = [...listeners, listener]
return () => {
listeners = listeners.filter(l => l !== listener)
}
},
getSnapshot: () => {
return todoList
}
}
function App() {
const todos = React.useSyncExternalStore(todoStore.subscribe, todoStore.getSnapshot)
return (
<>
{todos.map((todo) => (
<div key={todo.id}>{todo.text}</div>
))}
<button onClick={() => todoStore.addTodo({id: 4, text: 'Todo 4'})}>
添加 Todo
</button>
</>
)
}
当 App 组件执行时,useSyncExternalStore 调用 todoStore.subscribe,将 handleStoreChange 函数作为参数传入。点击按钮触发 Store 更新时,会触发 handleStoreChange 的执行,从而实现 React 状态的更新机制。
发布订阅模式的另一种应用
在不使用 useSyncExternalStore 的情况下,我们可以手动实现发布订阅模式来触发组件更新。当 store 更新时,通过发布订阅模式触发组件内部的 forceUpdate,从而实现组件状态的更新。
函数组件中使用 useState 配合自定义 Hook
// todo store(同上)
let todoList = [{id: 1, text: 'Todo 1'}, {id: 2, text: 'Todo 2'}, {id: 3, text: 'Todo 3'}]
let listeners = []
const emitChange = () => {
listeners.forEach(listener => listener())
}
const todoStore = {
addTodo: (todo) => {
todoList = [...todoList, todo]
emitChange()
},
subscribe: (listener) => {
listeners = [...listeners, listener]
return () => {
listeners = listeners.filter(l => l !== listener)
}
},
getSnapshot: () => {
return todoList
}
}
// 自定义 Hook 封装订阅逻辑
function useStore(store) {
// 使用 useState 的 setter 函数来触发组件重新渲染
const [, forceUpdate] = React.useState({})
React.useEffect(() => {
// 订阅 store 的变化
const unsubscribe = store.subscribe(() => {
// 通过更新 state 触发组件重新渲染
forceUpdate({})
})
// 组件卸载时取消订阅
return unsubscribe
}, [store])
return store.getSnapshot()
}
function App() {
const todos = useStore(todoStore)
return (
<>
{todos.map((todo) => (
<div key={todo.id}>{todo.text}</div>
))}
<button onClick={() => todoStore.addTodo({id: 4, text: 'Todo 4'})}>
添加 Todo
</button>
</>
)
}
这种方式的核心思想是:
- 在组件挂载时订阅 store 的变化
- 当 store 更新时,通过
emitChange通知所有订阅者 - 订阅者(组件)收到通知后,通过更新 state 来触发重新渲染
- 组件卸载时取消订阅,避免内存泄漏
useSyncExternalStore 本质上就是对这种模式的封装和优化,提供了更好的并发渲染支持和错误处理机制。