一. useSyncExternalStore
方法介绍
useSyncExternalStore
方法接收三个参数,第一个参数是监听事件函数,第二个参数是获取数据方法,第三个参数是服务端渲染时调用的方法
例如下面这段,监听window
的online
和offline
事件,当事件触发时会调用getSnapshot
方法获取新的返回值
function getSnapshot() {
return navigator.onLine
}
function subscribe(callback) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
function App() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot)
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>
}
二. 实现useSyncExternalStore
useSyncExternalStore
内部实现依赖useEffect
,可以参考文章手写React useEffect,理解useEffect原理
2.1 定义Hook
对象原型
每次调用React Hook
方法都会创建一个Hook
对象,多个Hook
对象之间通过next
指针进行索引,构成单链表数据结构
function Hook() {
this.memoizedState = null // 记录Hook数据
this.next = null // 记录下一个Hook对象
}
2.2 定义函数组件方法调用装饰器
在构建虚拟DOM
树阶段,每次调用函数组件方法(例如App Component Function
)时会执行renderWithHooks
方法,记录新FiberNode
节点,在调用React Hook
方法时会用到。
// 记录新FiberNode节点
let currentlyRenderingFiber = null
// 记录旧Hook对象
let currentHook = null
// 记录新Hook对象
let workInProgressHook = null
/**
* @param {*} workInProgress 新FiberNode节点
* @param {*} Component 函数组件方法
* @param {*} props 函数组件方法入参属性
*/
export function renderWithHooks(workInProgress, Component, props) {
// 记录新FiberNode节点
currentlyRenderingFiber = workInProgress
// 将FiberNode节点的updateQueue属性赋值为null,重新收集useEffect、useLayoutEffect、useInsertionEffect数据
workInProgress.updateQueue = null
// 调用组件方法获取child ReactElement
const children = Component(props)
currentlyRenderingFiber = null
currentHook = null
workInProgressHook = null
return children
}
2.3 首次调用useSyncExternalStore
当首次执行组件方法调用useSyncExternalStore
方法时,执行mountSyncExternalStore
方法逻辑
- 创建
Hook
对象,构建Hook
链表 - 监听
subscribe
方法,在DOM
更新阶段调用subscribe
方法 - 调用
getSnapshot
方法获取初始值,将初始值和getSnapshot
方法赋值给Hook
对象的memoizedState
属性,返回初始值
function forceStoreRerender(fiber) {
// 获取FiberRootNode对象
const root = getRootForUpdatedFiber(fiber)
// 修改FiberNode节点优先级
fiber.lanes |= SyncLane
if (fiber.alternate !== null) fiber.alternate.lanes |= SyncLane
// 触发更新渲染
scheduleUpdateOnFiber(root, SyncLane)
}
// 调用subscribe方法,并获取返回的destroy方法
function subscribeToStore(fiber, hook, subscribe) {
// 当触发监听事件时,会调用callback方法
const callback = () => {
const [prevSnapshot, getSnapshot] = hook.memoizedState
const nextSnapshot = getSnapshot()
// 比对新旧值是否相同,如果不相同更新Hook对象memoizedState属性,触发更新渲染
if (nextSnapshot !== prevSnapshot) {
hook.memoizedState[0] = nextSnapshot
forceStoreRerender(fiber)
}
}
return subscribe(callback)
}
function mountWorkInProgressHook() {
// 创建Hook对象
const hook = new Hook()
// 构建Hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook = workInProgressHook.next = hook
}
return hook
}
// 为了便于理解,这里不讨论传入服务端渲染调用方法的情况
function mountSyncExternalStore(subscribe, getSnapshot) {
// 创建Hook对象,构建Hook单链表
const hook = mountWorkInProgressHook()
// 监听subscribe函数,在DOM更新阶段调用subscribe方法
mountEffect(
subscribeToStore.bind(null, currentlyRenderingFiber, hook, subscribe),
[subscribe],
)
// 调用getSnapshot方法获取初始值
const nextSnapshot = getSnapshot()
hook.memoizedState = [nextSnapshot, getSnapshot]
return nextSnapshot
}
2.4 更新调用useSyncExternalStore
当触发更新渲染重新执行组件方法调用useSyncExternalStore
方法时,执行updateSyncExternalStore
方法逻辑
- 创建
Hook
对象,复制旧Hook
对象属性值,构建Hook
链表 - 重新赋值
getSnapshot
方法,确保调用最新的getSnapshot
方法 - 监听
subscribe
方法,如果发生变更,会在DOM
更新阶段重新执行subscribe
方法
function updateWorkInProgressHook() {
// 获取旧Hook对象
if (currentHook === null) {
currentHook = currentlyRenderingFiber.alternate.memoizedState
} else {
currentHook = currentHook.next
}
// 创建新Hook对象,复制旧Hook对象属性值
const hook = new Hook()
hook.memoizedState = currentHook.memoizedState
// 构建Hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook = workInProgressHook.next = hook
}
return hook
}
// 为了便于理解,这里不讨论传入服务端渲染调用方法的情况
function updateSyncExternalStore(subscribe, getSnapshot) {
// 创建Hook对象,复制旧Hook对象属性值,构建Hook链表
const hook = updateWorkInProgressHook()
// 由于getSnapshot方法可能发生变更,所以需要重新赋值确保调用最新的getSnapshot方法
hook.memoizedState[1] = getSnapshot
// 监听subscribe方法,如果发生变更,会在DOM更新阶段重新执行subscribe方法
updateEffect(
subscribeToStore.bind(null, currentlyRenderingFiber, hook, subscribe),
[subscribe],
)
return hook.memoizedState[0]
}
2.5 定义useSyncExternalStore
方法
如果新节点不存在旧FiberNode
节点,说明是首次调用函数组件方法,则调用mountSyncExternalStore
方法,否则调用updateSyncExternalStore
方法
export function useSyncExternalStore(subscribe, getSnapshot) {
const current = currentlyRenderingFiber.alternate
if (current === null) {
return mountSyncExternalStore(subscribe, getSnapshot)
} else {
return updateSyncExternalStore(subscribe, getSnapshot)
}
}