注:这个hook使用了发布订阅的订阅器即subscribe
一、使用
1.ts声明
export function useSyncExternalStore<Snapshot>(
// onStoreChage可以理解为用例里面todosStore.subscribe函数入参listener;
// onStoreChage对应react运行时函数subscribeToStore的handleStoreChange变量
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot?: () => Snapshot,
): Snapshot;
注:
useSyncExternalStorehook返回类型是Snapshot;根据useSyncExternalStore的第二个入参getSnapshotts类型也能自动推导出是getSnapshot函数返回值类型Snapshot
2.用例解析
官方用例
TSX文件
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
export default function TodosApp() {
// 添加订阅,返回Snapshot(快照)
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
return (
<>
<button onClick={() => todosStore.addTodo()}>Add todo</button>
<hr />
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
Store文件(第三方store)
一个简单的
发布订阅
// 这是一个第三方 store 的例子,
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];
export const todosStore = {
// 改变state
addTodo() {
todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
emitChange();
},
// 发起订阅有react触发
subscribe(listener) {
// push进监听队列
listeners = [...listeners, listener];
// return出一个移除订阅
return () => {
listeners = listeners.filter(l => l !== listener);
};
},
// 获取Snapshot(快照)
getSnapshot() {
return todos;
}
};
// 发布订阅触发监听事件
function emitChange() {
for (let listener of listeners) {
listener();
}
}
二、原理
1.挂载阶段关键函数
这里忽略
服务端渲染代码
mountSyncExternalStore
function mountSyncExternalStore<T>(
// useSyncExternalStore hook的第一个参数
subscribe: (() => void) => () => void,
// useSyncExternalStore hook的第二个参数,读取store中的Snapshot
getSnapshot: () => T,
): T {
const fiber = currentlyRenderingFiber;
const hook = mountWorkInProgressHook();
// 执行 getSnapshot 函数,读取store中的Snapshot。类比用例中todosStore.getSnapshot
nextSnapshot = getSnapshot();
// Unless we're rendering a blocking lane, schedule a consistency check.
// Right before committing, we will walk the tree and check if any of the
// stores were mutated.
//
// We won't do this if we're hydrating server-rendered content, because if
// the content is stale, it's already visible anyway. Instead we'll patch
// it up in a passive effect.
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
}
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
// Read the current snapshot from the store on every render. This breaks the
// normal rules of React, and only works because store updates are
// always synchronous.
// 缓存从外部store Snapshot,重新渲染时可以从hooks链表上获取当前hook的Snapshot
hook.memoizedState = nextSnapshot;
// 这个init对象很重要,存储了当前Snapshot和useSyncExternalStore的第二个参数getSnapshot函数
const inst: StoreInstance<T> = {
value: nextSnapshot,
getSnapshot,
};
hook.queue = inst;
// Schedule an effect to subscribe to the store.
// 挂载effect,订阅外部的store;subscribeToStore触发订阅;mountEffect类比useEffect第一次执行
mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
// Schedule an effect to update the mutable instance fields. We will update
// this whenever subscribe, getSnapshot, or value changes. Because there's no
// clean-up function, and we track the deps correctly, we can call pushEffect
// directly, without storing any additional state. For the same reason, we
// don't need to set a static flag, either.
// TODO: We can move this to the passive phase once we add a pre-commit
// consistency check. See the next comment.
fiber.flags |= PassiveEffect;
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
undefined,
null,
);
// 这里返回Snapshot,用例中todos
return nextSnapshot;
}
subscribeToStore
function subscribeToStore(fiber, inst, subscribe) {
const handleStoreChange = () => {
// The store changed. Check if the snapshot changed since the last time we
// read from the store.
// inst的值=>: const inst: StoreInstance<T> = { value: nextSnapshot, getSnapshot, };
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
// 强制重新渲染
forceStoreRerender(fiber);
}
};
// Subscribe to the store and return a clean-up function.
// 订阅store
return subscribe(handleStoreChange);
}
checkIfSnapshotChanged
function checkIfSnapshotChanged(inst) {
const latestGetSnapshot = inst.getSnapshot;
const prevValue = inst.value;
try {
const nextValue = latestGetSnapshot();
// 判断前后值是否一样
return !is(prevValue, nextValue);
} catch (error) {
return true;
}
}
2.更新阶段的关键函数
updateSyncExternalStore
function updateSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
const fiber = currentlyRenderingFiber;
const hook = updateWorkInProgressHook();
// Read the current snapshot from the store on every render. This breaks the
// normal rules of React, and only works because store updates are
// always synchronous.
const nextSnapshot = getSnapshot();
// 获取旧的Snapshot
const prevSnapshot = hook.memoizedState;
const snapshotChanged = !is(prevSnapshot, nextSnapshot);
if (snapshotChanged) {
// 更新hook memoizedState
hook.memoizedState = nextSnapshot;
// 标记更新
markWorkInProgressReceivedUpdate();
}
const inst = hook.queue;
// 根据subscribe内存地址是否变化重新执行useEffect
updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
subscribe,
]);
// Whenever getSnapshot or subscribe changes, we need to check in the
// commit phase if there was an interleaved mutation. In concurrent mode
// this can happen all the time, but even in synchronous mode, an earlier
// effect may have mutated the store.
if (
inst.getSnapshot !== getSnapshot ||
snapshotChanged ||
// Check if the susbcribe function changed. We can save some memory by
// checking whether we scheduled a subscription effect above.
(workInProgressHook !== null &&
workInProgressHook.memoizedState.tag & HookHasEffect)
) {
fiber.flags |= PassiveEffect;
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
undefined,
null,
);
// Unless we're rendering a blocking lane, schedule a consistency check.
// Right before committing, we will walk the tree and check if any of the
// stores were mutated.
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
}
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
// 返回新的Snapshot
return nextSnapshot;
}
3.组件卸载取消订阅
这部分很简单;可以理解为组件卸载执行useEffect return的函数,即取消订阅
mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
function subscribeToStore(fiber, inst, subscribe) {
const handleStoreChange = () => {
// The store changed. Check if the snapshot changed since the last time we
// read from the store.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceStoreRerender(fiber);
}
};
// Subscribe to the store and return a clean-up function.
// 订阅store,返回取消订阅
return subscribe(handleStoreChange);
}