Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

React useSyncExternalStore:基于发布订阅模式的状态订阅机制

基于 v19.2.0 版本

作者:oasis


在阅读 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>
        </>
    )
}

这种方式的核心思想是:

  1. 在组件挂载时订阅 store 的变化
  2. 当 store 更新时,通过 emitChange 通知所有订阅者
  3. 订阅者(组件)收到通知后,通过更新 state 来触发重新渲染
  4. 组件卸载时取消订阅,避免内存泄漏

useSyncExternalStore 本质上就是对这种模式的封装和优化,提供了更好的并发渲染支持和错误处理机制。

关联阅读