Skip to content

Commit c70d397

Browse files
committed
feat: useSyncExternalStore
1 parent 3e17f33 commit c70d397

File tree

4 files changed

+1003
-0
lines changed

4 files changed

+1003
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
3+
*
4+
* openInula is licensed under Mulan PSL v2.
5+
* You can use this software according to the terms and conditions of the Mulan PSL v2.
6+
* You may obtain a copy of Mulan PSL v2 at:
7+
*
8+
* http://license.coscl.org.cn/MulanPSL2
9+
*
10+
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
11+
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
12+
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
13+
* See the Mulan PSL v2 for more details.
14+
*/
15+
16+
import { isSame } from '../renderer/utils/compare';
17+
import {
18+
useState,
19+
useRef,
20+
useEffect,
21+
useLayoutEffect,
22+
useMemo
23+
} from '../renderer/hooks/HookExternal';
24+
25+
type StoreChangeListener = () => void;
26+
type Unsubscribe = () => void;
27+
type Store<T> = { value: T; getSnapshot: () => T }
28+
29+
export function useSyncExternalStore<T>(
30+
subscribe: (onStoreChange: StoreChangeListener) => Unsubscribe,
31+
getSnapshot: () => T,
32+
): T {
33+
// 获取当前Store快照
34+
const value = getSnapshot();
35+
36+
// 用于强制重新渲染和存储store引用,非普通state
37+
// 需要强制更新时调用 `forceUpdate({inst})`,由于是新的对象引入,会导致组件强制更新
38+
const [{ store }, forceUpdate] = useState<{ store: Store<T> }>({
39+
store: {
40+
value,
41+
getSnapshot
42+
}
43+
});
44+
45+
function reRenderIfStoreChange() {
46+
if (didSnapshotChange(store)) {
47+
forceUpdate({ store });
48+
}
49+
}
50+
51+
// 必须在 layout 阶段(绘制前)同步完成,以便后续的访问到正确的value。
52+
useLayoutEffect(() => {
53+
// 同步更新 快照值和 getSnapshot 函数引用,确保它们始终是最新的。
54+
// subscribe 更新,代表数据源变化,也需要更新
55+
store.value = value;
56+
store.getSnapshot = getSnapshot;
57+
58+
reRenderIfStoreChange();
59+
}, [subscribe, value, getSnapshot]);
60+
61+
62+
useEffect(() => {
63+
// useLayoutEffect和useEffect存在时机,store可能会变化
64+
reRenderIfStoreChange();
65+
66+
// 开始订阅,返回值以在卸载组件是取消订阅
67+
return subscribe(reRenderIfStoreChange);
68+
}, [subscribe]);
69+
70+
71+
return value;
72+
}
73+
74+
/**
75+
* 检查快照是否发生变化
76+
* @param store Store实例
77+
* @returns 返回Store状态是否发生变化
78+
*/
79+
function didSnapshotChange<T>(store: Store<T>) {
80+
const latestGetSnapshot = store.getSnapshot;
81+
const prevValue = store.value;
82+
try {
83+
const nextValue = latestGetSnapshot();
84+
return !isSame(prevValue, nextValue);
85+
} catch (error) {
86+
return true;
87+
}
88+
}
89+
90+
// 用于追踪已渲染快照的实例类型
91+
type SelectionInstance<Selection> =
92+
| { hasValue: true; value: Selection }
93+
| { hasValue: false; value: null };
94+
95+
// 确保返回的类型既是原始类型 T,又确实是个函数
96+
function isFunction<T>(value: T): value is T & ((...args: any[]) => any) {
97+
return typeof value !== 'function';
98+
}
99+
100+
// 与useSyncExternalStore相同,但支持选择器和相等性判断参数
101+
export function useSyncExternalStoreWithSelector<Snapshot, Selection>(
102+
subscribe: (onStoreChange: StoreChangeListener) => Unsubscribe,
103+
getSnapshot: () => Snapshot,
104+
getServerSnapshot: void | null | (() => Snapshot),
105+
selector: (snapshot: Snapshot) => Selection,
106+
isEqual?: (a: Selection, b: Selection) => boolean,
107+
): Selection {
108+
// 用于缓存已渲染的store快照值
109+
const instRef = useRef<SelectionInstance<Selection>>({
110+
hasValue: false,
111+
value: null
112+
});
113+
114+
// 创建带选择器的快照获取函数
115+
const snapshotWithSelector = useMemo(() => {
116+
// 使用闭包变量追踪缓存状态,在 getSnapshot / selector / isEqual 不变时
117+
let initialized = false;
118+
let cachedSnapshot: Snapshot;
119+
let cachedSelection: Selection;
120+
121+
const memoizedSelectorFn = (snapshot: Snapshot) => {
122+
// 首次调用时的处理
123+
if (!initialized) {
124+
initialized = true;
125+
cachedSnapshot = snapshot;
126+
const selection = selector(snapshot);
127+
128+
// 尝试复用当前渲染值
129+
if (isFunction(isEqual) && instRef.current.hasValue) {
130+
const current = instRef.current.value;
131+
if (isEqual(current, selection)) {
132+
cachedSelection = current;
133+
return current;
134+
}
135+
}
136+
137+
cachedSelection = selection;
138+
return selection;
139+
}
140+
141+
// 尝试复用之前的结果
142+
const previousSnapshot = cachedSnapshot;
143+
const previousSelection = cachedSelection;
144+
145+
// 快照未变化时直接返回之前的选择结果
146+
if (isSame(previousSnapshot, snapshot)) {
147+
return previousSelection;
148+
}
149+
150+
// 快照已变化,需要计算新的选择结果
151+
const newSelection = selector(snapshot);
152+
153+
// 使用自定义相等判断函数检查数据是否真正发生变化
154+
if (isFunction(isEqual) && isEqual(previousSelection, newSelection)) {
155+
// 虽然选择结果相等,但仍需更新快照避免保留旧引用
156+
cachedSnapshot = snapshot;
157+
return previousSelection;
158+
}
159+
160+
// 更新缓存并返回新结果
161+
cachedSnapshot = snapshot;
162+
cachedSelection = newSelection;
163+
return newSelection;
164+
};
165+
166+
return () => memoizedSelectorFn(getSnapshot());
167+
}, [getSnapshot, selector, isEqual]);
168+
169+
const selectedValue = useSyncExternalStore(subscribe, snapshotWithSelector);
170+
171+
// 更新实例状态
172+
useEffect(() => {
173+
instRef.current.hasValue = true;
174+
instRef.current.value = selectedValue;
175+
}, [selectedValue]);
176+
177+
return selectedValue;
178+
}

packages/inula/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373

7474
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
7575
import { toRaw } from './inulax/proxy/ProxyHandler';
76+
import { useSyncExternalStore, useSyncExternalStoreWithSelector } from './compat/UseSyncExternalStoreHook';
7677

7778
const Inula = {
7879
Children,
@@ -93,6 +94,8 @@ const Inula = {
9394
useReducer,
9495
useRef,
9596
useState,
97+
useSyncExternalStore,
98+
useSyncExternalStoreWithSelector,
9699
createElement,
97100
cloneElement,
98101
isValidElement,
@@ -146,6 +149,8 @@ export {
146149
useReducer,
147150
useRef,
148151
useState,
152+
useSyncExternalStore,
153+
useSyncExternalStoreWithSelector,
149154
createElement,
150155
cloneElement,
151156
isValidElement,

0 commit comments

Comments
 (0)