xdm,又要到饭了,又更新代码了!
总结一下上一篇完成的内容,
- 使用配合fiber实现了useMemo / useCallback的实现, 从底层理解运行机制。
有兴趣的可以点这里查看useMemo / useCallback的实现, 从底层理解运行机制
这一篇,我们探究一下 useSyncExternalStore,
大概2年之前,也就是这个api出来没有多久,我就用这个api实现了一个类zustand的工具。当时想着如果vue也有提供类似的就好了。
网上陆续也有不少文章讲解了如何使用它完成一个状态管理工具,但是却好像没有多少人讲过它的实现原理。那么我们就这里实现一个。
首先, 它的出现给第三方状态管理提供了一个官方稳定的api,帮助它们简化外部数据状态管理实现(官方可能想着有一统实现规则所以出了这个api,zustand源码就用了)。
要完成这个api,我们就需要先定义一个store。
const store = {
state: { count: 0},
listeners: new Set(),
subscribe(listener) {
this.listeners.push(listener)
return ()=>this.listeners.delete(listener)
},
setState(newState) {
this.state = {...this.state, ...newState}
this.listeners.forEach(listener=>listener())
},
getSnapshot() {
return this.state
}
}
实现useSyncExternalStore
function useSyncExternalStore (subscribe, getSnapshot) {
const oldHook = workInProgress.alternate && workInProgress.alternate.hooks && workInProgress.alternate.hooks[workInProgress.currentHook]
const hook = {
snapshot: oldHook ? oldHook.snapshot : getSnapshot()
deps: [subscribe, getSnapshot],
cleanup: null
}
workInProgress.hooks.push(hook)
workInProgress.currentHook++
const fiber = workInProgress
useEffect(()=>{
const checkForUpdates = ()=>{
const newSnapshot = getSnapshot()
if (newSnapshot !== hook.snapshot) {
hook.snapshot = newSnapshot
scheduleUpdateOnFiber(fiber)
}
}
const unsubscribe = subscribe(checkForUpdates)
return ()=>{
if (unsubscribe) {
unsubscribe()
}
}
}, [subscribe, getSnapshot])
return hook.snapshot
}
我们实现了这个hook,但是开始讲解源码实现逻辑前,带着这样一个问题,面试时候如果有人问既然有了useContext ,为什么还会有状态管理比如zustand 等,它们的实现和解决的问题以及哪些时候用哪个比较好。
- 首先从从
current fiber 树找出对应当前fiber的数据(即复用),这里尝试找出snapshot(即state的值) - 创建hook,有3个属性,snapshot , deps, cleanup, 接着添加hook进fiber的hooks数组,最后currentHook 索引更新。直至这里,没有启动任何更新。在这创建的hook就是下次渲染的oldHook(帮助你理解避免你没有阅读之前的文章)
- 接着使用useEffect (useEffect 的源码实现之前几篇已经实现过了,有兴趣的可以查看),调用store 的subscribe 方法,这个方法接受一个函数参数(listener),每当state 更新都会遍历listeners数组,依次运行listeners。
- 每一个listener 都会判断store的当前state 是否与上次的state 相等,如果不想等,则启动react对于当前fiber的更新(scheduleUpdateOnFiber(fiber))
- subscribe 返回对于当前传入listener的对应的删除操作。
- 最后返回 hook.snapshot , 即此次渲染的store 的state 值
那么源码分析可以清楚的知道,实现原理应用了发布订阅模式,即收集所有使用了useSyncExternalStore的组件。如果某次某个fiber的更新发现上次的state 不等于现在的state则对所有订阅了useSyncExternalStore的组件进行更新。
那么来回答一下上面的问题,
useContext 和 useSyncExternalStore 是 React 中用于管理和共享状态的两个不同钩子(Hook),它们各自有不同的使用场景和性能特性。要比较它们的性能优劣,首先需要了解它们的设计目的和工作机制。
useContext 简介
useContext 是 React 提供的一个钩子,用于在组件树中共享数据,而无需通过逐层传递 props。它通常与 React.createContext 一起使用,适用于共享全局状态或主题等数据。
特点:
- 简便易用:非常适合简单的全局状态管理。
- 自动重新渲染:当上下文值变化时,所有使用该上下文的组件都会重新渲染。
- 易于理解和实现。
性能考量:
- 重新渲染范围:上下文值变化会导致所有使用该上下文的组件重新渲染,即使这些组件只使用了上下文的一部分数据。这在上下文值频繁变化且组件树较大时,可能会引起性能问题。
- 优化手段:可以通过拆分上下文、使用
React.memo等方式来减少不必要的重新渲染。
useSyncExternalStore 简介
useSyncExternalStore 是 React 18 引入的一个钩子,主要用于订阅外部数据源(如 Redux、MobX 等)的变化。它旨在为并发模式(Concurrent Mode)提供更好的支持,确保外部数据源的一致性和同步性。
特点:
- 外部订阅:专为订阅外部存储设计,适合处理外部状态管理库。
- 避免不必要的渲染:通过内部优化,只在必要时触发组件更新。
- 并发模式支持: 设计上考虑了并发渲染,确保数据的一致性。
性能考量:
- 精细的更新控制:相比
useContext,useSyncExternalStore能更精细地控制何时触发组件更新,减少不必要的渲染。 - 适用于复杂状态管理:在复杂的状态管理场景下,性能表现更优,因为它避免了整个上下文值变化导致的全局重新渲染问题。
性能对比
-
重新渲染的粒度:
useContext:当上下文值变化时,所有消费该上下文的组件都会重新渲染,即使这些组件只依赖上下文的一部分数据 (这个其实有办法解决)。useSyncExternalStore:仅当外部存储实际变化时,才会触发相关组件的更新,避免了不必要的全局重新渲染。
-
适用场景:
useContext:适用于简单的全局状态或配置信息的共享,如主题、语言设置等。useSyncExternalStore:适用于需要高性能、精细化控制的外部状态管理,如大型应用中的复杂状态库。
-
并发模式支持:
useContext:在并发模式下,useContext可能会因为频繁的重新渲染而导致性能问题。useSyncExternalStore:专为并发模式设计,能够更好地处理并发渲染带来的挑战,确保数据一致性和性能优化。
何时选择使用哪一个?
-
使用
useContext:- 适用于简单、静态的全局状态共享。
- 状态变化频率较低,且不涉及复杂的更新逻辑。
- 项目较小或状态管理需求简单。
-
使用
useSyncExternalStore:- 适用于需要订阅外部数据源的复杂应用。
- 状态变化频繁,需要精细化控制组件更新。
- 需要在并发模式下保持高性能和数据一致性。
- 使用了诸如 Redux、MobX 等外部状态管理库。
即然这里提及了useContext, 那么下一篇将从0-1开发useContext 。
如果这样的长度/强度你觉得可以接受,觉得有帮助,可以继续阅读下一篇,实现一个 Mini React:核心功能详解 - useContext的实现 。
如果文章对你有帮助,请点个赞支持一下!
啥也不是,散会。