Teact: Align effect order with other libraries (#6685)
This commit is contained in:
parent
846ec883d4
commit
1f0ac5b276
@ -24,7 +24,7 @@ import usePrevious from '../hooks/usePrevious';
|
||||
import { useSignalEffect } from '../hooks/useSignalEffect';
|
||||
import { getIsInBackground } from '../hooks/window/useBackgroundMode';
|
||||
|
||||
// import Test from './test/TestSvg';
|
||||
// import Test from './test/TestCleanupOrder';
|
||||
import Auth from './auth/Auth';
|
||||
import Notifications from './common/Notifications';
|
||||
import UiLoader from './common/UiLoader';
|
||||
|
||||
@ -1,53 +1,84 @@
|
||||
import { useEffect, useLayoutEffect, useState } from '../../lib/teact/teact';
|
||||
|
||||
const TestCleanupOrder = () => {
|
||||
const [, setRand] = useState(Math.random());
|
||||
/* eslint-disable no-console */
|
||||
import { type TeactNode, useEffect, useLayoutEffect, useState } from '../../lib/teact/teact';
|
||||
|
||||
function Component1({ children }: { children?: TeactNode }) {
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('effect 1');
|
||||
|
||||
setTimeout(() => {
|
||||
setRand(Math.random());
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('cleanup 1');
|
||||
};
|
||||
console.log('Effect 1');
|
||||
return () => console.log('Cleanup 1');
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('effect 2');
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('cleanup 2');
|
||||
};
|
||||
console.log('Effect 1.2');
|
||||
return () => console.log('Cleanup 1.2');
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('layout effect 1');
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('layout cleanup 1');
|
||||
};
|
||||
console.log('Layout 1');
|
||||
return () => console.log('Layout Cleanup 1');
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('layout effect 2');
|
||||
console.log('Layout 1.2');
|
||||
return () => console.log('Layout Cleanup 1.2');
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('layout cleanup 2');
|
||||
};
|
||||
function Component1B({ children }: { children?: TeactNode }) {
|
||||
useEffect(() => {
|
||||
console.log('Effect 1b');
|
||||
return () => console.log('Cleanup 1b');
|
||||
});
|
||||
|
||||
return <div>Test</div>;
|
||||
};
|
||||
useLayoutEffect(() => {
|
||||
console.log('Layout 1b');
|
||||
return () => console.log('Layout Cleanup 1b');
|
||||
});
|
||||
return 'B';
|
||||
}
|
||||
|
||||
function Component2({ children }: { children?: TeactNode }) {
|
||||
useEffect(() => {
|
||||
console.log('Effect 2');
|
||||
return () => console.log('Cleanup 2');
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
console.log('Layout 2');
|
||||
return () => console.log('Layout Cleanup 2');
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
function Component3() {
|
||||
useEffect(() => {
|
||||
console.log('Effect 3');
|
||||
return () => console.log('Cleanup 3');
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
console.log('Layout 3');
|
||||
return () => console.log('Layout Cleanup 3');
|
||||
});
|
||||
return <div>Leaf</div>;
|
||||
}
|
||||
|
||||
function TestCleanupOrder() {
|
||||
const [isMounted, setIsMounted] = useState(true);
|
||||
return (
|
||||
<div style="padding: 1rem" onClick={() => setIsMounted((p) => !p)}>
|
||||
{isMounted && (
|
||||
<>
|
||||
<Component1>
|
||||
<Component2>
|
||||
<Component3 />
|
||||
</Component2>
|
||||
</Component1>
|
||||
<Component1B />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TestCleanupOrder;
|
||||
|
||||
@ -155,6 +155,7 @@ const Fragment = Symbol('Fragment') as unknown as FC<{ children: TeactNode }>;
|
||||
const DEBUG_RENDER_THRESHOLD = 7;
|
||||
const DEBUG_EFFECT_THRESHOLD = 7;
|
||||
const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']);
|
||||
const MAX_EFFECT_CURSORS_PER_INSTANCE = 10000;
|
||||
|
||||
let contextCounter = 0;
|
||||
|
||||
@ -315,12 +316,37 @@ if (DEBUG) {
|
||||
|
||||
let instancesPendingUpdate = new Set<ComponentInstance>();
|
||||
let idsToExcludeFromUpdate = new Set<number>();
|
||||
let pendingEffects = new Map<string, Effect>();
|
||||
let pendingCleanups = new Map<string, EffectCleanup>();
|
||||
let pendingLayoutEffects = new Map<string, Effect>();
|
||||
let pendingLayoutCleanups = new Map<string, EffectCleanup>();
|
||||
let pendingEffects = new Map<number, Effect>();
|
||||
let pendingCleanups = new Map<number, EffectCleanup>();
|
||||
let pendingLayoutEffects = new Map<number, Effect>();
|
||||
let pendingLayoutCleanups = new Map<number, EffectCleanup>();
|
||||
let areImmediateEffectsCaptured = false;
|
||||
|
||||
/*
|
||||
Effect call order:
|
||||
- Regular call order inside component
|
||||
- Child to parent
|
||||
- Parent to child on unmount
|
||||
- Sibling behavior is not defined
|
||||
*/
|
||||
function runEffectsMap<T extends NoneToVoidFunction>(map: Map<number, T>) {
|
||||
Array.from(map.entries())
|
||||
.sort(([aKey], [bKey]) => {
|
||||
const idA = Math.floor(aKey / MAX_EFFECT_CURSORS_PER_INSTANCE);
|
||||
const idB = Math.floor(bKey / MAX_EFFECT_CURSORS_PER_INSTANCE);
|
||||
|
||||
const idDiff = idB - idA;
|
||||
if (idDiff !== 0) {
|
||||
return idDiff;
|
||||
}
|
||||
|
||||
const cursorA = aKey % MAX_EFFECT_CURSORS_PER_INSTANCE;
|
||||
const cursorB = bKey % MAX_EFFECT_CURSORS_PER_INSTANCE;
|
||||
|
||||
return cursorA - cursorB;
|
||||
}).forEach(([_, cb]) => void cb());
|
||||
}
|
||||
|
||||
/*
|
||||
Order:
|
||||
- component effect cleanups
|
||||
@ -350,11 +376,11 @@ const runUpdatePassOnRaf = throttleWith(requestMeasure, () => {
|
||||
|
||||
const currentCleanups = pendingCleanups;
|
||||
pendingCleanups = new Map();
|
||||
currentCleanups.forEach((cb) => cb());
|
||||
runEffectsMap(currentCleanups);
|
||||
|
||||
const currentEffects = pendingEffects;
|
||||
pendingEffects = new Map();
|
||||
currentEffects.forEach((cb) => cb());
|
||||
runEffectsMap(currentEffects);
|
||||
|
||||
requestMutation(() => {
|
||||
instancesToUpdate.forEach(prepareComponentForFrame);
|
||||
@ -382,11 +408,11 @@ export function captureImmediateEffects() {
|
||||
function runCapturedImmediateEffects() {
|
||||
const currentLayoutCleanups = pendingLayoutCleanups;
|
||||
pendingLayoutCleanups = new Map();
|
||||
currentLayoutCleanups.forEach((cb) => cb());
|
||||
runEffectsMap(currentLayoutCleanups);
|
||||
|
||||
const currentLayoutEffects = pendingLayoutEffects;
|
||||
pendingLayoutEffects = new Map();
|
||||
currentLayoutEffects.forEach((cb) => cb());
|
||||
runEffectsMap(currentLayoutEffects);
|
||||
|
||||
areImmediateEffectsCaptured = false;
|
||||
}
|
||||
@ -738,6 +764,11 @@ function useEffectBase(
|
||||
}
|
||||
|
||||
renderingInstance.hooks.effects.cursor++;
|
||||
|
||||
if (DEBUG && cursor >= MAX_EFFECT_CURSORS_PER_INSTANCE) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[Teact] Effect cursor #${cursor} in ${componentInstance.name} is out of bounds. How?`);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleEffect(
|
||||
@ -750,7 +781,7 @@ function scheduleEffect(
|
||||
const cleanup = byCursor[cursor]?.cleanup;
|
||||
const cleanupsContainer = isLayout ? pendingLayoutCleanups : pendingCleanups;
|
||||
const effectsContainer = isLayout ? pendingLayoutEffects : pendingEffects;
|
||||
const effectId = `${componentInstance.id}_${cursor}`;
|
||||
const effectId = componentInstance.id * MAX_EFFECT_CURSORS_PER_INSTANCE + cursor;
|
||||
|
||||
if (cleanup) {
|
||||
const runEffectCleanup = () => safeExec(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user