@reckona/mreact-compat 0.0.174 → 0.0.175
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/dist/element.d.ts +1 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +35 -1
- package/dist/element.js.map +1 -1
- package/dist/host-reconciler.d.ts.map +1 -1
- package/dist/host-reconciler.js +187 -23
- package/dist/host-reconciler.js.map +1 -1
- package/dist/reactive-prop-cell.d.ts +8 -0
- package/dist/reactive-prop-cell.d.ts.map +1 -1
- package/dist/reactive-prop-cell.js +108 -4
- package/dist/reactive-prop-cell.js.map +1 -1
- package/package.json +3 -3
- package/src/element.ts +49 -1
- package/src/host-reconciler.ts +272 -34
- package/src/reactive-prop-cell.ts +154 -4
package/src/host-reconciler.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
STRICT_MODE_TYPE,
|
|
13
13
|
Suspense,
|
|
14
14
|
SuspenseList,
|
|
15
|
+
type MemoType,
|
|
15
16
|
type ReactCompatElement,
|
|
16
17
|
type ReactCompatPortal,
|
|
17
18
|
type ReactiveDomBlockProps,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
type ReactCompatNode,
|
|
22
23
|
} from "./element.js";
|
|
23
24
|
import {
|
|
25
|
+
batchReactivePropCellUpdates,
|
|
24
26
|
createReactivePropCell,
|
|
25
27
|
createReactivePropProxy,
|
|
26
28
|
setReactivePropCell,
|
|
@@ -227,13 +229,18 @@ export function renderHostFiberRoot(
|
|
|
227
229
|
): Fiber {
|
|
228
230
|
const workInProgress = createWorkInProgress(root.current, { children: element });
|
|
229
231
|
const rootDocument = root.container.ownerDocument;
|
|
230
|
-
const result =
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
232
|
+
const result = batchReactivePropCellUpdates(() =>
|
|
233
|
+
reconcileHostChild(
|
|
234
|
+
workInProgress,
|
|
235
|
+
root.current.child,
|
|
236
|
+
element,
|
|
237
|
+
runtime,
|
|
238
|
+
options.previousNodes === undefined ? "0" : "",
|
|
239
|
+
{
|
|
240
|
+
...options,
|
|
241
|
+
documentRef: options.documentRef ?? rootDocument,
|
|
242
|
+
},
|
|
243
|
+
),
|
|
237
244
|
);
|
|
238
245
|
workInProgress.child = result.fiber;
|
|
239
246
|
workInProgress.memoizedProps = { children: element };
|
|
@@ -778,6 +785,7 @@ function isKeyedMemoRowCandidate(node: ReactCompatNode): boolean {
|
|
|
778
785
|
// reconciler can drive its committed block straight through the prop cell instead
|
|
779
786
|
// of re-invoking the component (the block's bound DOM updates via subscriptions).
|
|
780
787
|
const STATIC_REACTIVE_BLOCK_MARKER = "__mreactStaticBlock";
|
|
788
|
+
const MEMO_COMPARE_PROPS_MARKER = "__mreactMemoCompareProps";
|
|
781
789
|
|
|
782
790
|
function isStaticReactiveBlockComponent(type: unknown): boolean {
|
|
783
791
|
return (
|
|
@@ -786,6 +794,118 @@ function isStaticReactiveBlockComponent(type: unknown): boolean {
|
|
|
786
794
|
);
|
|
787
795
|
}
|
|
788
796
|
|
|
797
|
+
function areCompilerMemoComparePropsEqual(
|
|
798
|
+
memoType: MemoType<Record<string, unknown>>,
|
|
799
|
+
previousProps: Record<string, unknown>,
|
|
800
|
+
nextProps: Record<string, unknown>,
|
|
801
|
+
): boolean | undefined {
|
|
802
|
+
const keys = memoType[MEMO_COMPARE_PROPS_MARKER];
|
|
803
|
+
if (keys === undefined) {
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (keys.length === 1) {
|
|
808
|
+
const key = keys[0] as string;
|
|
809
|
+
return previousProps[key] === nextProps[key];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (keys.length === 2) {
|
|
813
|
+
const first = keys[0] as string;
|
|
814
|
+
const second = keys[1] as string;
|
|
815
|
+
return previousProps[first] === nextProps[first] &&
|
|
816
|
+
previousProps[second] === nextProps[second];
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (keys.length === 3) {
|
|
820
|
+
const first = keys[0] as string;
|
|
821
|
+
const second = keys[1] as string;
|
|
822
|
+
const third = keys[2] as string;
|
|
823
|
+
return previousProps[first] === nextProps[first] &&
|
|
824
|
+
previousProps[second] === nextProps[second] &&
|
|
825
|
+
previousProps[third] === nextProps[third];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
for (const key of keys) {
|
|
829
|
+
if (previousProps[key] !== nextProps[key]) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function tryCreateInitialStaticBlockMemoFiber(
|
|
838
|
+
node: ReactCompatElement,
|
|
839
|
+
key: string | undefined,
|
|
840
|
+
memoType: {
|
|
841
|
+
type: ReactCompatElement["type"];
|
|
842
|
+
compare?: (
|
|
843
|
+
previous: Record<string, unknown>,
|
|
844
|
+
next: Record<string, unknown>,
|
|
845
|
+
) => boolean;
|
|
846
|
+
},
|
|
847
|
+
runtime: RootRuntime,
|
|
848
|
+
memoPath: string,
|
|
849
|
+
options: FiberHydrationOptions,
|
|
850
|
+
): FiberReconcileResult | undefined {
|
|
851
|
+
if (
|
|
852
|
+
options.previousNodes !== undefined ||
|
|
853
|
+
node.ref !== null ||
|
|
854
|
+
!isStaticReactiveBlockComponent(memoType.type)
|
|
855
|
+
) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const props = node.props as Record<string, unknown>;
|
|
860
|
+
const componentType = memoType.type as (props: Record<string, unknown>) => ReactCompatNode;
|
|
861
|
+
const rendered = componentType(props);
|
|
862
|
+
|
|
863
|
+
if (!isReactCompatElement(rendered) || rendered.type !== REACTIVE_DOM_BLOCK_TYPE) {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const memoFiber = createFiber("memo", props, key);
|
|
868
|
+
memoFiber.type = memoType;
|
|
869
|
+
|
|
870
|
+
const componentFiber = createFiber("function-component", props, key);
|
|
871
|
+
componentFiber.type = componentType;
|
|
872
|
+
|
|
873
|
+
const childResult = createHostFiber(
|
|
874
|
+
componentFiber,
|
|
875
|
+
undefined,
|
|
876
|
+
rendered,
|
|
877
|
+
key,
|
|
878
|
+
runtime,
|
|
879
|
+
`${memoPath}.0`,
|
|
880
|
+
options,
|
|
881
|
+
);
|
|
882
|
+
componentFiber.child = childResult.fiber;
|
|
883
|
+
if (componentFiber.child !== undefined) {
|
|
884
|
+
componentFiber.child.return = componentFiber;
|
|
885
|
+
componentFiber.child.sibling = undefined;
|
|
886
|
+
bubbleHostChild(componentFiber, componentFiber.child);
|
|
887
|
+
}
|
|
888
|
+
componentFiber.stateNode = {
|
|
889
|
+
element: node,
|
|
890
|
+
props,
|
|
891
|
+
instanceKeys: emptyInstanceKeys,
|
|
892
|
+
hasContextDependencies: false,
|
|
893
|
+
} satisfies FunctionFiberState;
|
|
894
|
+
|
|
895
|
+
memoFiber.child = componentFiber;
|
|
896
|
+
componentFiber.return = memoFiber;
|
|
897
|
+
bubbleHostChild(memoFiber, componentFiber);
|
|
898
|
+
memoFiber.memoizedState = {
|
|
899
|
+
props,
|
|
900
|
+
instanceKeys: emptyInstanceKeys,
|
|
901
|
+
hasDirtyInstanceDependencies: false,
|
|
902
|
+
hasUnflushedEffectDependencies: false,
|
|
903
|
+
hasRetainedInstanceDependencies: false,
|
|
904
|
+
} satisfies MemoFiberState;
|
|
905
|
+
|
|
906
|
+
return { fiber: memoFiber, consumed: childResult.consumed };
|
|
907
|
+
}
|
|
908
|
+
|
|
789
909
|
// In-place bailout for a keyed memo row whose props are unchanged and which has
|
|
790
910
|
// no hook/context/effect dependencies. The caller has proven the key order is
|
|
791
911
|
// unchanged (hasSameKeyOrderPrefix), so the fiber keeps its position and can be
|
|
@@ -814,16 +934,21 @@ function tryReuseDependencyFreeKeyedMemoRow(
|
|
|
814
934
|
state === undefined ||
|
|
815
935
|
state.hasDirtyInstanceDependencies !== false ||
|
|
816
936
|
state.hasUnflushedEffectDependencies !== false ||
|
|
817
|
-
state.hasRetainedInstanceDependencies !== false
|
|
818
|
-
!areMemoPropsEqual(
|
|
819
|
-
child.type as { compare?: (a: Record<string, unknown>, b: Record<string, unknown>) => boolean },
|
|
820
|
-
state.props,
|
|
821
|
-
child.props as Record<string, unknown>,
|
|
822
|
-
)
|
|
937
|
+
state.hasRetainedInstanceDependencies !== false
|
|
823
938
|
) {
|
|
824
939
|
return undefined;
|
|
825
940
|
}
|
|
826
941
|
|
|
942
|
+
const memoType = child.type as MemoType<Record<string, unknown>>;
|
|
943
|
+
const nextProps = child.props as Record<string, unknown>;
|
|
944
|
+
const propsEqual =
|
|
945
|
+
areCompilerMemoComparePropsEqual(memoType, state.props, nextProps) ??
|
|
946
|
+
areMemoPropsEqual(memoType, state.props, nextProps);
|
|
947
|
+
|
|
948
|
+
if (!propsEqual) {
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
|
|
827
952
|
matched.pendingProps = child.props;
|
|
828
953
|
matched.flags = NoFlags;
|
|
829
954
|
matched.subtreeFlags = NoFlags;
|
|
@@ -890,8 +1015,10 @@ function tryCellUpdateStaticBlockMemoRow(
|
|
|
890
1015
|
|
|
891
1016
|
// Drive the bound DOM through the prop cell (the marker guarantees the block's
|
|
892
1017
|
// props are the component's props verbatim), then reuse the memo fiber in place.
|
|
893
|
-
|
|
894
|
-
|
|
1018
|
+
const nextProps = child.props as Record<string, unknown>;
|
|
1019
|
+
setReactivePropCell(blockState.propCell, nextProps);
|
|
1020
|
+
state.props = nextProps;
|
|
1021
|
+
matched.memoizedState = state;
|
|
895
1022
|
matched.pendingProps = child.props;
|
|
896
1023
|
matched.flags = NoFlags;
|
|
897
1024
|
matched.subtreeFlags = NoFlags;
|
|
@@ -903,12 +1030,15 @@ function tryCellUpdateStaticBlockMemoRow(
|
|
|
903
1030
|
}
|
|
904
1031
|
|
|
905
1032
|
// Fast path for a keyed list of memo-wrapped rows (e.g. `<RowMemo key={id} />`)
|
|
906
|
-
// whose key order is UNCHANGED
|
|
907
|
-
// "select row"
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
1033
|
+
// whose key order is UNCHANGED, append-only, or a single deletion between
|
|
1034
|
+
// renders — the js-framework-benchmark "select row", "partial update",
|
|
1035
|
+
// "append rows", and "remove row" shapes. Walks current fibers and new children
|
|
1036
|
+
// in lockstep: unchanged rows bail in place (no allocation, no general
|
|
1037
|
+
// keyed-reconcile machinery), changed rows cell-update through the proven
|
|
1038
|
+
// per-child path, appended rows are created as a suffix, and a single deleted
|
|
1039
|
+
// row is removed while the suffix is retained. Any reorder / insert /
|
|
1040
|
+
// multi-delete / non-memo child makes it return undefined so the general
|
|
1041
|
+
// reconcile takes over.
|
|
912
1042
|
function reconcileKeyedMemoRowHostChildren(
|
|
913
1043
|
parent: Fiber,
|
|
914
1044
|
currentFirstChild: Fiber | undefined,
|
|
@@ -922,12 +1052,16 @@ function reconcileKeyedMemoRowHostChildren(
|
|
|
922
1052
|
currentFirstChild === undefined ||
|
|
923
1053
|
runtime === undefined ||
|
|
924
1054
|
options.previousNodes !== undefined ||
|
|
925
|
-
!isKeyedMemoRowCandidate(children[0])
|
|
926
|
-
!hasSameKeyOrderPrefix(currentFirstChild, children)
|
|
1055
|
+
!isKeyedMemoRowCandidate(children[0])
|
|
927
1056
|
) {
|
|
928
1057
|
return undefined;
|
|
929
1058
|
}
|
|
930
1059
|
|
|
1060
|
+
const memoRowType = (children[0] as ReactCompatElement).type;
|
|
1061
|
+
if (!hasSameAppendOrSingleDeleteKeyOrder(currentFirstChild, children, memoRowType)) {
|
|
1062
|
+
return undefined;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
931
1065
|
let currentKeyed: Fiber | undefined = currentFirstChild;
|
|
932
1066
|
let first: Fiber | undefined;
|
|
933
1067
|
let previous: Fiber | undefined;
|
|
@@ -935,22 +1069,38 @@ function reconcileKeyedMemoRowHostChildren(
|
|
|
935
1069
|
let subtreeChildListChanged = false;
|
|
936
1070
|
let hasRefSubtree = false;
|
|
937
1071
|
let hasDisposableResources = false;
|
|
1072
|
+
let listShapeChanged = false;
|
|
1073
|
+
let appendSuffix: AppendSuffixCommitHint | undefined;
|
|
1074
|
+
let removedFiber: Fiber | undefined;
|
|
938
1075
|
let dirtyChildren: Fiber[] | undefined;
|
|
939
1076
|
|
|
940
1077
|
for (let index = 0; index < children.length; index += 1) {
|
|
941
|
-
const child = children[index];
|
|
1078
|
+
const child = children[index] as ReactCompatElement;
|
|
1079
|
+
const childElement = child;
|
|
1080
|
+
let matched: Fiber | undefined = currentKeyed;
|
|
942
1081
|
|
|
943
|
-
if (
|
|
1082
|
+
if (matched === undefined) {
|
|
1083
|
+
// append-only suffix
|
|
1084
|
+
} else if (matched.key === childElement.key) {
|
|
1085
|
+
currentKeyed = matched.sibling;
|
|
1086
|
+
} else if (
|
|
1087
|
+
removedFiber === undefined &&
|
|
1088
|
+
matched.sibling?.key === childElement.key &&
|
|
1089
|
+
canSkipSingleDeletedKeyedFiber(children, index, matched.sibling)
|
|
1090
|
+
) {
|
|
1091
|
+
removedFiber = matched;
|
|
1092
|
+
matched = matched.sibling;
|
|
1093
|
+
currentKeyed = matched.sibling;
|
|
1094
|
+
listShapeChanged = true;
|
|
1095
|
+
} else {
|
|
944
1096
|
return undefined;
|
|
945
1097
|
}
|
|
946
1098
|
|
|
947
|
-
const matched = currentKeyed;
|
|
948
|
-
currentKeyed = currentKeyed.sibling;
|
|
949
|
-
const childElement = child as ReactCompatElement;
|
|
950
|
-
|
|
951
1099
|
let fiber =
|
|
952
|
-
|
|
953
|
-
|
|
1100
|
+
matched === undefined
|
|
1101
|
+
? undefined
|
|
1102
|
+
: tryReuseDependencyFreeKeyedMemoRow(matched, childElement) ??
|
|
1103
|
+
tryCellUpdateStaticBlockMemoRow(matched, childElement);
|
|
954
1104
|
if (fiber === undefined) {
|
|
955
1105
|
const result = createHostFiber(
|
|
956
1106
|
parent,
|
|
@@ -968,11 +1118,16 @@ function reconcileKeyedMemoRowHostChildren(
|
|
|
968
1118
|
return undefined;
|
|
969
1119
|
}
|
|
970
1120
|
|
|
971
|
-
if (fiber !== matched && fiber.alternate !== matched) {
|
|
1121
|
+
if (matched !== undefined && fiber !== matched && fiber.alternate !== matched) {
|
|
972
1122
|
markOptimizedChildForDeletion(parent, matched);
|
|
973
1123
|
}
|
|
974
1124
|
}
|
|
975
1125
|
|
|
1126
|
+
if (matched === undefined) {
|
|
1127
|
+
listShapeChanged = true;
|
|
1128
|
+
appendSuffix ??= { fiber, index };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
976
1131
|
if (first === undefined) {
|
|
977
1132
|
first = fiber;
|
|
978
1133
|
} else if (previous !== undefined) {
|
|
@@ -1003,18 +1158,84 @@ function reconcileKeyedMemoRowHostChildren(
|
|
|
1003
1158
|
}
|
|
1004
1159
|
|
|
1005
1160
|
if (currentKeyed !== undefined) {
|
|
1006
|
-
|
|
1161
|
+
if (removedFiber !== undefined || currentKeyed.sibling !== undefined) {
|
|
1162
|
+
return undefined;
|
|
1163
|
+
}
|
|
1164
|
+
removedFiber = currentKeyed;
|
|
1165
|
+
listShapeChanged = true;
|
|
1007
1166
|
}
|
|
1008
1167
|
|
|
1009
1168
|
parent.hasRefSubtree = hasRefSubtree;
|
|
1010
1169
|
parent.hasDisposableResources = hasDisposableResources;
|
|
1011
1170
|
parent.subtreeFlags = subtreeFlags;
|
|
1012
1171
|
parent.subtreeChildListChanged = subtreeChildListChanged;
|
|
1013
|
-
parent.childListChanged =
|
|
1172
|
+
parent.childListChanged = listShapeChanged;
|
|
1173
|
+
if (appendSuffix !== undefined && canStoreAppendSuffixCommitHint(parent)) {
|
|
1174
|
+
parent.memoizedState = appendSuffix;
|
|
1175
|
+
}
|
|
1176
|
+
if (removedFiber !== undefined) {
|
|
1177
|
+
parent.deletions = [removedFiber];
|
|
1178
|
+
markOptimizedChildForDeletion(parent, removedFiber);
|
|
1179
|
+
}
|
|
1014
1180
|
recordDirtyChildCommitHints(parent, dirtyChildren);
|
|
1015
1181
|
return { fiber: first, consumed: 0 };
|
|
1016
1182
|
}
|
|
1017
1183
|
|
|
1184
|
+
function hasSameAppendOrSingleDeleteKeyOrder(
|
|
1185
|
+
currentFirstChild: Fiber,
|
|
1186
|
+
children: readonly ReactCompatNode[],
|
|
1187
|
+
expectedType: ReactCompatElement["type"],
|
|
1188
|
+
): boolean {
|
|
1189
|
+
let current: Fiber | undefined = currentFirstChild;
|
|
1190
|
+
let skippedDeletion = false;
|
|
1191
|
+
|
|
1192
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
1193
|
+
const child = children[index];
|
|
1194
|
+
if (!isSameKeyedMemoRowCandidate(child, expectedType)) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const key = (child as ReactCompatElement).key;
|
|
1199
|
+
|
|
1200
|
+
if (current === undefined) {
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (current.key === key) {
|
|
1205
|
+
current = current.sibling;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const sibling = current.sibling;
|
|
1210
|
+
if (
|
|
1211
|
+
!skippedDeletion &&
|
|
1212
|
+
sibling !== undefined &&
|
|
1213
|
+
sibling.key === key &&
|
|
1214
|
+
canSkipSingleDeletedKeyedFiber(children, index, sibling)
|
|
1215
|
+
) {
|
|
1216
|
+
skippedDeletion = true;
|
|
1217
|
+
current = sibling.sibling;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return current === undefined || (!skippedDeletion && current.sibling === undefined);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function isSameKeyedMemoRowCandidate(
|
|
1228
|
+
node: ReactCompatNode,
|
|
1229
|
+
expectedType: ReactCompatElement["type"],
|
|
1230
|
+
): boolean {
|
|
1231
|
+
return (
|
|
1232
|
+
isReactCompatElement(node) &&
|
|
1233
|
+
node.key !== null &&
|
|
1234
|
+
node.ref === null &&
|
|
1235
|
+
node.type === expectedType
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1018
1239
|
function canReuseDependencyFreeMemoAtKey(
|
|
1019
1240
|
current: Fiber | undefined,
|
|
1020
1241
|
node: ReactCompatElement,
|
|
@@ -1926,6 +2147,22 @@ function createHostFiberImpl(
|
|
|
1926
2147
|
const memoPath = `${path}.memo`;
|
|
1927
2148
|
const previousMemoFiber =
|
|
1928
2149
|
current?.tag === "memo" && current.type === memoType ? current : undefined;
|
|
2150
|
+
const initialStaticBlockMemoFiber =
|
|
2151
|
+
previousMemoFiber === undefined
|
|
2152
|
+
? tryCreateInitialStaticBlockMemoFiber(
|
|
2153
|
+
node,
|
|
2154
|
+
key,
|
|
2155
|
+
memoType,
|
|
2156
|
+
runtime,
|
|
2157
|
+
memoPath,
|
|
2158
|
+
options,
|
|
2159
|
+
)
|
|
2160
|
+
: undefined;
|
|
2161
|
+
|
|
2162
|
+
if (initialStaticBlockMemoFiber !== undefined) {
|
|
2163
|
+
return initialStaticBlockMemoFiber;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1929
2166
|
const previousMemoState =
|
|
1930
2167
|
previousMemoFiber !== undefined
|
|
1931
2168
|
? (previousMemoFiber.memoizedState as MemoFiberState | undefined)
|
|
@@ -2935,6 +3172,7 @@ function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
|
|
|
2935
3172
|
}
|
|
2936
3173
|
|
|
2937
3174
|
if (removedAny) {
|
|
3175
|
+
disposeHostFiberResources(removed);
|
|
2938
3176
|
fiber.deletions = undefined;
|
|
2939
3177
|
}
|
|
2940
3178
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { batch } from "@reckona/mreact-reactive-core";
|
|
1
2
|
import {
|
|
2
3
|
flushQueuedComputations,
|
|
3
4
|
notifySubscribers,
|
|
@@ -15,6 +16,8 @@ import type { ReactiveDomBlockResult } from "./element.js";
|
|
|
15
16
|
export interface ReactivePropCell {
|
|
16
17
|
value: Record<string, unknown>;
|
|
17
18
|
source: Source;
|
|
19
|
+
propertySources?: Map<PropertyKey, Source> | undefined;
|
|
20
|
+
propertySnapshots?: Map<PropertyKey, ShallowObjectSnapshot> | undefined;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
// What a reactive-dom-block fiber stores in stateNode: the committed node and
|
|
@@ -23,17 +26,35 @@ export interface ReactiveDomBlockState extends ReactiveDomBlockResult {
|
|
|
23
26
|
propCell?: ReactivePropCell | undefined;
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
let propCellBatchDepth = 0;
|
|
30
|
+
let propCellBatchNeedsFlush = false;
|
|
31
|
+
|
|
26
32
|
export function createReactivePropCell(props: Record<string, unknown>): ReactivePropCell {
|
|
27
33
|
return { value: props, source: { subscribers: null } };
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
export function batchReactivePropCellUpdates<T>(run: () => T): T {
|
|
37
|
+
propCellBatchDepth += 1;
|
|
38
|
+
try {
|
|
39
|
+
return batch(run);
|
|
40
|
+
} finally {
|
|
41
|
+
propCellBatchDepth -= 1;
|
|
42
|
+
if (propCellBatchDepth === 0 && propCellBatchNeedsFlush) {
|
|
43
|
+
propCellBatchNeedsFlush = false;
|
|
44
|
+
flushQueuedComputations();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
const PROP_PROXY_HANDLER: ProxyHandler<ReactivePropCell> = {
|
|
31
50
|
get(cell, property) {
|
|
32
|
-
trackSource(cell
|
|
33
|
-
|
|
51
|
+
trackSource(getReactivePropPropertySource(cell, property));
|
|
52
|
+
const value = Reflect.get(cell.value, property);
|
|
53
|
+
rememberReactivePropObjectSnapshot(cell, property, value);
|
|
54
|
+
return value;
|
|
34
55
|
},
|
|
35
56
|
has(cell, property) {
|
|
36
|
-
trackSource(cell
|
|
57
|
+
trackSource(getReactivePropPropertySource(cell, property));
|
|
37
58
|
return property in cell.value;
|
|
38
59
|
},
|
|
39
60
|
ownKeys(cell) {
|
|
@@ -50,6 +71,107 @@ export function createReactivePropProxy<P extends object>(cell: ReactivePropCell
|
|
|
50
71
|
return new Proxy(cell, PROP_PROXY_HANDLER) as unknown as P;
|
|
51
72
|
}
|
|
52
73
|
|
|
74
|
+
function getReactivePropPropertySource(cell: ReactivePropCell, property: PropertyKey): Source {
|
|
75
|
+
let propertySources = cell.propertySources;
|
|
76
|
+
|
|
77
|
+
if (propertySources === undefined) {
|
|
78
|
+
propertySources = new Map();
|
|
79
|
+
cell.propertySources = propertySources;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let source = propertySources.get(property);
|
|
83
|
+
|
|
84
|
+
if (source === undefined) {
|
|
85
|
+
source = { subscribers: null };
|
|
86
|
+
propertySources.set(property, source);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return source;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface ShallowObjectSnapshot {
|
|
93
|
+
value: object;
|
|
94
|
+
entries: Map<PropertyKey, unknown>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function rememberReactivePropObjectSnapshot(
|
|
98
|
+
cell: ReactivePropCell,
|
|
99
|
+
property: PropertyKey,
|
|
100
|
+
value: unknown,
|
|
101
|
+
): void {
|
|
102
|
+
if (!isObjectLike(value)) {
|
|
103
|
+
cell.propertySnapshots?.delete(property);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let snapshots = cell.propertySnapshots;
|
|
108
|
+
if (snapshots === undefined) {
|
|
109
|
+
snapshots = new Map();
|
|
110
|
+
cell.propertySnapshots = snapshots;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
snapshots.set(property, createShallowObjectSnapshot(value));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createShallowObjectSnapshot(value: object): ShallowObjectSnapshot {
|
|
117
|
+
const entries = new Map<PropertyKey, unknown>();
|
|
118
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
119
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
120
|
+
if (descriptor !== undefined && "value" in descriptor) {
|
|
121
|
+
entries.set(key, descriptor.value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { value, entries };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isObjectLike(value: unknown): value is object {
|
|
129
|
+
return (typeof value === "object" && value !== null) || typeof value === "function";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasShallowObjectSnapshotChanged(
|
|
133
|
+
snapshot: ShallowObjectSnapshot | undefined,
|
|
134
|
+
nextValue: unknown,
|
|
135
|
+
): boolean {
|
|
136
|
+
if (snapshot === undefined || snapshot.value !== nextValue || !isObjectLike(nextValue)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const key of Reflect.ownKeys(nextValue)) {
|
|
141
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(nextValue, key);
|
|
142
|
+
if (descriptor !== undefined && "value" in descriptor) {
|
|
143
|
+
if (!snapshot.entries.has(key) || !Object.is(snapshot.entries.get(key), descriptor.value)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const key of snapshot.entries.keys()) {
|
|
150
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(nextValue, key);
|
|
151
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function shouldNotifyReactivePropProperty(
|
|
160
|
+
previous: Record<string, unknown>,
|
|
161
|
+
next: Record<string, unknown>,
|
|
162
|
+
property: PropertyKey,
|
|
163
|
+
snapshot: ShallowObjectSnapshot | undefined,
|
|
164
|
+
): boolean {
|
|
165
|
+
if ((property in previous) !== (property in next)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const previousValue = Reflect.get(previous, property);
|
|
170
|
+
const nextValue = Reflect.get(next, property);
|
|
171
|
+
return !Object.is(previousValue, nextValue) ||
|
|
172
|
+
hasShallowObjectSnapshotChanged(snapshot, nextValue);
|
|
173
|
+
}
|
|
174
|
+
|
|
53
175
|
export function setReactivePropCell(
|
|
54
176
|
cell: ReactivePropCell,
|
|
55
177
|
next: Record<string, unknown>,
|
|
@@ -58,10 +180,38 @@ export function setReactivePropCell(
|
|
|
58
180
|
return;
|
|
59
181
|
}
|
|
60
182
|
|
|
183
|
+
const previous = cell.value;
|
|
184
|
+
const propertySources = cell.propertySources;
|
|
185
|
+
let notified = false;
|
|
186
|
+
|
|
61
187
|
cell.value = next;
|
|
62
188
|
|
|
189
|
+
if (propertySources !== undefined) {
|
|
190
|
+
for (const [property, source] of propertySources) {
|
|
191
|
+
if (
|
|
192
|
+
shouldNotifyReactivePropProperty(
|
|
193
|
+
previous,
|
|
194
|
+
next,
|
|
195
|
+
property,
|
|
196
|
+
cell.propertySnapshots?.get(property),
|
|
197
|
+
)
|
|
198
|
+
) {
|
|
199
|
+
notifySubscribers(source);
|
|
200
|
+
notified = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
63
205
|
if (cell.source.subscribers !== null) {
|
|
64
206
|
notifySubscribers(cell.source);
|
|
65
|
-
|
|
207
|
+
notified = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (notified) {
|
|
211
|
+
if (propCellBatchDepth === 0) {
|
|
212
|
+
flushQueuedComputations();
|
|
213
|
+
} else {
|
|
214
|
+
propCellBatchNeedsFlush = true;
|
|
215
|
+
}
|
|
66
216
|
}
|
|
67
217
|
}
|