@hsu-react/ui 2.5.5 → 2.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/components/Chart/Bar/index.js +6 -1
- package/es/components/Chart/Bubble/index.js +6 -1
- package/es/components/Chart/Common/index.js +6 -1
- package/es/components/Chart/Gauge/index.js +6 -1
- package/es/components/Chart/Heatmap/index.js +9 -2
- package/es/components/Chart/Line/index.js +6 -1
- package/es/components/Chart/Pie/Pie3D/index.js +15 -7
- package/es/components/Chart/Pie/index.js +9 -2
- package/es/components/Chart/Polar/index.js +9 -2
- package/es/components/Chart/Radar/index.js +6 -1
- package/es/components/Checkbox/CheckboxGroup/index.js +4 -1
- package/es/components/FormItem/FormCodeMirror/index.js +3 -1
- package/es/components/FormItem/ItemContainer/index.js +15 -5
- package/es/components/Operate/index.js +4 -1
- package/es/components/Pagination/index.js +3 -1
- package/es/components/Search/_hooks/useSearchCommon.js +4 -1
- package/es/components/Select/AutoCompleteSelect/index.js +3 -1
- package/es/components/Spreadsheet/index.js +8 -2
- package/es/components/Table/_hooks/useTableColumns.js +4 -1
- package/es/components/Tree/_hooks/useTreeState.js +7 -4
- package/es/components/Tree/index.js +8 -2
- package/es/hooks/useShallowStable.d.ts +32 -0
- package/es/hooks/useShallowStable.js +54 -0
- package/lib/components/Chart/Bar/index.js +6 -1
- package/lib/components/Chart/Bubble/index.js +6 -1
- package/lib/components/Chart/Common/index.js +6 -1
- package/lib/components/Chart/Gauge/index.js +6 -1
- package/lib/components/Chart/Heatmap/index.js +9 -2
- package/lib/components/Chart/Line/index.js +6 -1
- package/lib/components/Chart/Pie/Pie3D/index.js +15 -7
- package/lib/components/Chart/Pie/index.js +9 -2
- package/lib/components/Chart/Polar/index.js +9 -2
- package/lib/components/Chart/Radar/index.js +6 -1
- package/lib/components/Checkbox/CheckboxGroup/index.js +4 -1
- package/lib/components/FormItem/FormCodeMirror/index.js +3 -1
- package/lib/components/FormItem/ItemContainer/index.js +16 -6
- package/lib/components/Operate/index.js +4 -1
- package/lib/components/Pagination/index.js +3 -1
- package/lib/components/Search/_hooks/useSearchCommon.js +4 -1
- package/lib/components/Select/AutoCompleteSelect/index.js +3 -1
- package/lib/components/Spreadsheet/index.js +8 -2
- package/lib/components/Table/_hooks/useTableColumns.js +4 -1
- package/lib/components/Tree/_hooks/useTreeState.js +6 -3
- package/lib/components/Tree/index.js +8 -2
- package/lib/hooks/useShallowStable.d.ts +32 -0
- package/lib/hooks/useShallowStable.js +59 -0
- package/package.json +1 -1
- package/src/__tests__/callbackDepsGuard.test.ts +4 -51
- package/src/__tests__/depsScan.ts +205 -0
- package/src/__tests__/freshObjectDeps.test.tsx +380 -0
- package/src/__tests__/freshObjectDepsGuard.test.ts +175 -0
- package/src/components/Chart/Bar/index.tsx +6 -1
- package/src/components/Chart/Bubble/index.tsx +6 -1
- package/src/components/Chart/Common/index.tsx +6 -1
- package/src/components/Chart/Gauge/index.tsx +6 -1
- package/src/components/Chart/Heatmap/index.tsx +20 -12
- package/src/components/Chart/Line/index.tsx +6 -1
- package/src/components/Chart/Pie/Pie3D/index.tsx +12 -3
- package/src/components/Chart/Pie/index.tsx +10 -2
- package/src/components/Chart/Polar/index.tsx +10 -2
- package/src/components/Chart/Radar/index.tsx +6 -1
- package/src/components/Chart/optionMemo.test.tsx +232 -0
- package/src/components/Checkbox/CheckboxGroup/index.tsx +4 -1
- package/src/components/FormItem/FormCodeMirror/index.tsx +4 -1
- package/src/components/FormItem/ItemContainer/index.tsx +21 -6
- package/src/components/Operate/index.tsx +4 -1
- package/src/components/Pagination/index.tsx +4 -1
- package/src/components/Search/_hooks/useSearchCommon.ts +4 -1
- package/src/components/Select/AutoCompleteSelect/index.tsx +4 -1
- package/src/components/Spreadsheet/depsMemo.test.tsx +95 -0
- package/src/components/Spreadsheet/index.tsx +9 -2
- package/src/components/Table/_hooks/useTableColumns.tsx +4 -1
- package/src/components/Tree/_hooks/useTreeState.ts +14 -8
- package/src/components/Tree/depsMemo.test.tsx +64 -0
- package/src/components/Tree/index.tsx +9 -2
- package/src/hooks/useShallowStable.ts +65 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import React, { ReactElement } from "react";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 2.5.7:`xOptions` 的解构默认值与 `xOptionsRest` 这个 rest 包每渲染都是新对象,
|
|
7
|
+
* 直接进「建表格实例」那条 effect 的依赖数组 → **effect 每渲染必重跑**。
|
|
8
|
+
* 实例本身被 `!sheet` 挡住了不会重建,但 effect 体每次都白跑一遍,
|
|
9
|
+
* 且依赖里挂着的 `xOptionsRest` 永远比不出「配置真的变了」——纯粹的假依赖。
|
|
10
|
+
*
|
|
11
|
+
* 这里单独一个文件,是因为要把 `x-data-spreadsheet` 换成假实现(jsdom 里跑不了真的)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const probe = { effect: 0 };
|
|
15
|
+
(globalThis as Record<string, unknown>).__spreadsheetProbe = probe;
|
|
16
|
+
|
|
17
|
+
vi.mock("react", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import("react")>();
|
|
19
|
+
const counters = () =>
|
|
20
|
+
(globalThis as Record<string, unknown>).__spreadsheetProbe as typeof probe;
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
useEffect: (fn: () => unknown, deps: unknown[]) =>
|
|
24
|
+
actual.useEffect(() => {
|
|
25
|
+
counters().effect++;
|
|
26
|
+
return fn() as ReturnType<React.EffectCallback>;
|
|
27
|
+
}, deps),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const construct = vi.fn();
|
|
32
|
+
const loadData = vi.fn();
|
|
33
|
+
|
|
34
|
+
vi.mock("x-data-spreadsheet", () => ({
|
|
35
|
+
default: class FakeSpreadsheet {
|
|
36
|
+
constructor(...args: unknown[]) {
|
|
37
|
+
construct(...args);
|
|
38
|
+
}
|
|
39
|
+
loadData(...args: unknown[]) {
|
|
40
|
+
loadData(...args);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
vi.mock("x-data-spreadsheet/dist/xspreadsheet.css", () => ({}));
|
|
46
|
+
|
|
47
|
+
import Spreadsheet from ".";
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
probe.effect = 0;
|
|
51
|
+
construct.mockClear();
|
|
52
|
+
loadData.mockClear();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function rerenderEffectCost(make: () => ReactElement) {
|
|
56
|
+
const { rerender, unmount } = render(make());
|
|
57
|
+
const before = probe.effect;
|
|
58
|
+
rerender(make());
|
|
59
|
+
const cost = probe.effect - before;
|
|
60
|
+
unmount();
|
|
61
|
+
return cost;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("Spreadsheet:xOptions / xOptionsRest 不再让建表 effect 每渲染必重跑", () => {
|
|
65
|
+
it("配置没变时重渲染不再重跑任何 effect", () => {
|
|
66
|
+
// 不传 xOptions(走默认值)与传内联对象(消费方最常见的写法)都必须为 0
|
|
67
|
+
expect(rerenderEffectCost(() => <Spreadsheet />)).toBe(0);
|
|
68
|
+
expect(
|
|
69
|
+
rerenderEffectCost(() => <Spreadsheet xOptions={{ showBottomTool: true }} />),
|
|
70
|
+
).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("表格实例只建一次,重渲染不会重复 new", () => {
|
|
74
|
+
const { rerender } = render(<Spreadsheet />);
|
|
75
|
+
rerender(<Spreadsheet />);
|
|
76
|
+
rerender(<Spreadsheet />);
|
|
77
|
+
expect(construct).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("xOptions 真变了仍然透进新配置(浅比较不相等就立刻透出)", () => {
|
|
81
|
+
// 首帧就带上配置,确认它确实进了 XSpreadsheet 的构造参数
|
|
82
|
+
render(<Spreadsheet xOptions={{ showBottomTool: false, mode: "read" }} />);
|
|
83
|
+
expect(construct).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(construct.mock.calls[0][1]).toMatchObject({ mode: "read" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("data 真变了仍然重新 loadData", () => {
|
|
88
|
+
const bookA = { SheetNames: ["A"], Sheets: { A: {} } } as never;
|
|
89
|
+
const bookB = { SheetNames: ["B"], Sheets: { B: {} } } as never;
|
|
90
|
+
const { rerender } = render(<Spreadsheet data={bookA} />);
|
|
91
|
+
const first = loadData.mock.calls.length;
|
|
92
|
+
rerender(<Spreadsheet data={bookB} />);
|
|
93
|
+
expect(loadData.mock.calls.length).toBeGreaterThan(first);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -6,6 +6,7 @@ import "x-data-spreadsheet/dist/xspreadsheet.css";
|
|
|
6
6
|
import classNames from "classnames";
|
|
7
7
|
import { WorkBook } from "xlsx";
|
|
8
8
|
import { stox } from "./xlsxspread";
|
|
9
|
+
import useShallowStable from "../../hooks/useShallowStable";
|
|
9
10
|
|
|
10
11
|
interface XOptions extends Omit<Options, "view"> {
|
|
11
12
|
showBottomTool?: boolean;
|
|
@@ -17,9 +18,15 @@ interface SpreadsheetProps {
|
|
|
17
18
|
className?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// 解构默认值写成字面量会每次渲染新建一个对象,进依赖数组就让 effect 每渲染必重跑;提到模块级常量
|
|
22
|
+
const EMPTY_X_OPTIONS: XOptions = {};
|
|
23
|
+
|
|
20
24
|
const Spreadsheet: React.FC<SpreadsheetProps> = (props) => {
|
|
21
|
-
const { data, xOptions =
|
|
22
|
-
const { showBottomTool = true, ...
|
|
25
|
+
const { data, xOptions = EMPTY_X_OPTIONS, className } = props;
|
|
26
|
+
const { showBottomTool = true, ...restXOptions } = xOptions;
|
|
27
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 memo 恒不命中。
|
|
28
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
29
|
+
const xOptionsRest = useShallowStable(restXOptions);
|
|
23
30
|
const ref = useRef<HTMLDivElement>(null);
|
|
24
31
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
25
32
|
const id = useMemo(() => generateRandomStr(10), []);
|
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
import usePermissions from "../../../hooks/usePermissions";
|
|
20
20
|
import { PaginationProps } from "../../Pagination";
|
|
21
21
|
|
|
22
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
23
|
+
const EMPTY_COLUMNS: ColumnsType = [];
|
|
24
|
+
|
|
22
25
|
// measureAlign columns: measured at 14px, consistent with the table body and autoWidth title measurement
|
|
23
26
|
const MEASURE_ALIGN_FONT_SIZE = 14;
|
|
24
27
|
// Canvas measureText only returns the advance width, about 1px narrower than the actual DOM rendering; add a 2px buffer to avoid squeezing the widest row
|
|
@@ -97,7 +100,7 @@ const useTableColumns = <T extends AnyObject>(
|
|
|
97
100
|
handleTableChange: TableProps<T>["onChange"];
|
|
98
101
|
} => {
|
|
99
102
|
const {
|
|
100
|
-
columns =
|
|
103
|
+
columns = EMPTY_COLUMNS,
|
|
101
104
|
scroll,
|
|
102
105
|
autoWidth,
|
|
103
106
|
dataSource,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, Key, useMemo, useRef } from "react";
|
|
1
|
+
import { useEffect, useState, Key, useMemo, useRef, useCallback } from "react";
|
|
2
2
|
import { Equal } from "hsu-utils";
|
|
3
3
|
import { CheckedKeys, TreeData } from "..";
|
|
4
4
|
import { normalizeCheckedKeys } from "../_utils/normalizeCheckedKeys";
|
|
@@ -42,13 +42,19 @@ export const useExpandedKeys = (
|
|
|
42
42
|
}
|
|
43
43
|
}, [defaultExpandedKeys, expandedKeysProps]);
|
|
44
44
|
|
|
45
|
-
// Wrap setExpandedKeys to mark that the user has manually interacted
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
// Wrap setExpandedKeys to mark that the user has manually interacted.
|
|
46
|
+
// 必须 useCallback:这个函数被 Tree 的 handleExpand 当依赖用,
|
|
47
|
+
// 每渲染新建一个就让那个 useCallback 恒不命中——展开回调每渲染换引用透给 antd Tree。
|
|
48
|
+
// 函数体只碰 ref 和状态 setter,两者都恒定,所以依赖数组是空的。
|
|
49
|
+
const handleSetExpandedKeys = useCallback(
|
|
50
|
+
(
|
|
51
|
+
keys: Key[] | undefined | ((prev: Key[] | undefined) => Key[] | undefined)
|
|
52
|
+
) => {
|
|
53
|
+
hasUserInteractedRef.current = true;
|
|
54
|
+
setExpandedKeys(keys);
|
|
55
|
+
},
|
|
56
|
+
[]
|
|
57
|
+
);
|
|
52
58
|
|
|
53
59
|
return [expandedKeys, handleSetExpandedKeys] as const;
|
|
54
60
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 2.5.7:`treeConfig` 是 rest 解构包,每次渲染都是新对象。它进了 `handleExpand`
|
|
7
|
+
* 的 `useCallback` 依赖数组 → **展开回调每渲染都换一个新引用**,原样传给 antd Tree。
|
|
8
|
+
*
|
|
9
|
+
* 这里把 antd 的 `Tree` 换成一个只记录 props 的探针,直接比 `onExpand` 的引用。
|
|
10
|
+
* 单独一个文件,是因为 `vi.mock` 是文件级的,不能污染别的用例。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const seen: Record<string, unknown>[] = [];
|
|
14
|
+
|
|
15
|
+
vi.mock("antd", async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal<typeof import("antd")>();
|
|
17
|
+
const Probe = (props: Record<string, unknown>) => {
|
|
18
|
+
seen.push(props);
|
|
19
|
+
return React.createElement(actual.Tree, props);
|
|
20
|
+
};
|
|
21
|
+
Probe.DirectoryTree = actual.Tree.DirectoryTree;
|
|
22
|
+
Probe.TreeNode = actual.Tree.TreeNode;
|
|
23
|
+
return { ...actual, Tree: Probe };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import Tree from ".";
|
|
27
|
+
|
|
28
|
+
const NODES = [{ key: "1", value: "1", title: "节点甲" }];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
seen.length = 0;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("Tree:treeConfig 不再让展开回调每渲染换引用", () => {
|
|
35
|
+
it("props 没变时,传给 antd Tree 的 onExpand 保持同一个引用", () => {
|
|
36
|
+
const make = () => <Tree treeData={NODES} />;
|
|
37
|
+
const { rerender } = render(make());
|
|
38
|
+
rerender(make());
|
|
39
|
+
expect(seen.length).toBeGreaterThanOrEqual(2);
|
|
40
|
+
expect(seen[seen.length - 1].onExpand).toBe(seen[0].onExpand);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("消费方传内联的透传配置时也保持同一个引用(浅比较看的是值)", () => {
|
|
44
|
+
const make = () => <Tree treeData={NODES} showLine blockNode />;
|
|
45
|
+
const { rerender } = render(make());
|
|
46
|
+
rerender(make());
|
|
47
|
+
expect(seen[seen.length - 1].onExpand).toBe(seen[0].onExpand);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("透传配置真变了仍然透出新引用,且新的 onExpand 调的是新的回调", () => {
|
|
51
|
+
const first = vi.fn();
|
|
52
|
+
const second = vi.fn();
|
|
53
|
+
const { rerender } = render(<Tree treeData={NODES} onExpand={first} />);
|
|
54
|
+
type Expand = (keys: unknown[], info: unknown) => void;
|
|
55
|
+
const before = seen[seen.length - 1].onExpand as Expand;
|
|
56
|
+
rerender(<Tree treeData={NODES} onExpand={second} />);
|
|
57
|
+
const after = seen[seen.length - 1].onExpand as Expand;
|
|
58
|
+
expect(after).not.toBe(before);
|
|
59
|
+
|
|
60
|
+
after(["1"], { node: { key: "1" } });
|
|
61
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(first).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -21,6 +21,7 @@ import { ButtonProps } from "../Button";
|
|
|
21
21
|
import { getExpandedKeysByLevel, getNodePath } from "./_utils";
|
|
22
22
|
import TextEllipsis from "../TextEllipsis";
|
|
23
23
|
import usePermissions from "../../hooks/usePermissions";
|
|
24
|
+
import useShallowStable from "../../hooks/useShallowStable";
|
|
24
25
|
import { isLegacyHasSelectorBrowser } from "../../utils/cssSupports";
|
|
25
26
|
|
|
26
27
|
export interface TreeData extends Record<string, unknown> {
|
|
@@ -79,6 +80,9 @@ interface TreeFC extends React.FC<TreeProps> {
|
|
|
79
80
|
TreeNode: typeof AntdTree.TreeNode;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
84
|
+
const EMPTY_TREE_DATA: TreeData[] = [];
|
|
85
|
+
|
|
82
86
|
const Tree = ((props: TreeProps) => {
|
|
83
87
|
const {
|
|
84
88
|
title,
|
|
@@ -88,7 +92,7 @@ const Tree = ((props: TreeProps) => {
|
|
|
88
92
|
titleClassName,
|
|
89
93
|
className,
|
|
90
94
|
search,
|
|
91
|
-
treeData =
|
|
95
|
+
treeData = EMPTY_TREE_DATA,
|
|
92
96
|
onChange,
|
|
93
97
|
onCheck,
|
|
94
98
|
onSelect,
|
|
@@ -109,8 +113,11 @@ const Tree = ((props: TreeProps) => {
|
|
|
109
113
|
onSelectPath,
|
|
110
114
|
allowDeselect = true,
|
|
111
115
|
titleSearchBarClassName,
|
|
112
|
-
...
|
|
116
|
+
...restTreeConfig
|
|
113
117
|
} = props;
|
|
118
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 memo 恒不命中。
|
|
119
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
120
|
+
const treeConfig = useShallowStable(restTreeConfig);
|
|
114
121
|
const { permitted } = usePermissions(hasPermi);
|
|
115
122
|
const legacyHasSelector = isLegacyHasSelectorBrowser();
|
|
116
123
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 把一个**内容没变、但每次渲染都新建**的对象收敛成**引用恒定**的对象:
|
|
5
|
+
* 浅比较(自有键 + `Object.is` 比值)相等就继续返回上一次那个引用。
|
|
6
|
+
*
|
|
7
|
+
* 用途只有一个:让它可以直接进 `useMemo` / `useEffect` 的依赖数组。
|
|
8
|
+
*
|
|
9
|
+
* React 的依赖数组按 `Object.is` 比引用,所以依赖里只要混进一个「每次渲染都新建
|
|
10
|
+
* 的对象」,memo 就**恒不命中**、effect 就**每渲染必重跑**。组件内部最容易出现
|
|
11
|
+
* 这种对象的地方是 rest 解构:
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { className, style, ...coreOption } = props; // coreOption 每次都是新对象
|
|
15
|
+
* const option = useMemo(() => ({ ...coreOption }), [coreOption]); // 永远不命中
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* 消费方就算把每一个 prop 都 memo 好,也躲不掉——新对象是组件自己建的。
|
|
19
|
+
* 正确写法是让这个对象回到值语义:
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const { className, style, ...restOption } = props;
|
|
23
|
+
* const coreOption = useShallowStable(restOption); // 内容不变 → 引用不变
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* 注意这**不是**「缓存结果跳过副作用」:memo 该重算时照样重算、effect 该重跑时
|
|
27
|
+
* 照样重跑——只要 rest 里任何一个值真的变了,浅比较就不相等,新引用立刻透出去。
|
|
28
|
+
*
|
|
29
|
+
* 渲染期间读写 ref 的安全性:并发渲染下被丢弃的那次渲染可能已经把 ref 换成了它
|
|
30
|
+
* 自己的对象,但那个对象与当次 props 浅相等,所以后续渲染拿到的引用**内容永远
|
|
31
|
+
* 与当前 props 一致**,不会读到过期值。
|
|
32
|
+
*/
|
|
33
|
+
export function useShallowStable<T extends object>(value: T): T {
|
|
34
|
+
const ref = useRef(value);
|
|
35
|
+
|
|
36
|
+
if (!shallowEqual(ref.current, value)) {
|
|
37
|
+
ref.current = value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return ref.current;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shallowEqual(a: object, b: object) {
|
|
44
|
+
if (Object.is(a, b)) return true;
|
|
45
|
+
|
|
46
|
+
const aKeys = Object.keys(a);
|
|
47
|
+
const bKeys = Object.keys(b);
|
|
48
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
49
|
+
|
|
50
|
+
for (const key of aKeys) {
|
|
51
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
52
|
+
if (
|
|
53
|
+
!Object.is(
|
|
54
|
+
(a as Record<string, unknown>)[key],
|
|
55
|
+
(b as Record<string, unknown>)[key],
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default useShallowStable;
|