Teact: Always run effects before component renders

This commit is contained in:
Alexander Zinchuk 2023-02-17 02:31:30 +01:00
parent deeda69f77
commit 6b235729f4
3 changed files with 947 additions and 39 deletions

910
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,6 @@
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { DEBUG, DEBUG_MORE } from '../../config'; import { DEBUG, DEBUG_MORE } from '../../config';
import { import { throttleWithRafFallback } from '../../util/schedulers';
throttleWithRafFallback,
throttleWithPrimaryRafFallback,
throttleWithTickEnd,
throttleWithPrimaryTickEnd,
} from '../../util/schedulers';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
import { getUnequalProps } from '../../util/arePropsShallowEqual'; import { getUnequalProps } from '../../util/arePropsShallowEqual';
import { handleError } from '../../util/handleError'; import { handleError } from '../../util/handleError';
@ -127,6 +122,9 @@ export type TeactNode =
| boolean | boolean
| TeactNode[]; | TeactNode[];
type Effect = () => (NoneToVoidFunction | void);
type EffectCleanup = NoneToVoidFunction;
const Fragment = Symbol('Fragment'); const Fragment = Symbol('Fragment');
const DEBUG_RENDER_THRESHOLD = 7; const DEBUG_RENDER_THRESHOLD = 7;
@ -306,6 +304,10 @@ document.addEventListener('dblclick', () => {
let instancesPendingUpdate = new Set<ComponentInstance>(); let instancesPendingUpdate = new Set<ComponentInstance>();
let idsToExcludeFromUpdate = new Set<number>(); 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>();
const runUpdatePassOnRaf = throttleWithRafFallback(() => { const runUpdatePassOnRaf = throttleWithRafFallback(() => {
idsToExcludeFromUpdate = new Set(); idsToExcludeFromUpdate = new Set();
@ -316,9 +318,15 @@ const runUpdatePassOnRaf = throttleWithRafFallback(() => {
instancesPendingUpdate = new Set(); instancesPendingUpdate = new Set();
instancesToUpdate.forEach((instance) => { const currentCleanups = pendingCleanups;
prepareComponentForFrame(instance); pendingCleanups = new Map();
}); currentCleanups.forEach((cb) => cb());
const currentEffects = pendingEffects;
pendingEffects = new Map();
currentEffects.forEach((cb) => cb());
instancesToUpdate.forEach(prepareComponentForFrame);
instancesToUpdate.forEach((instance) => { instancesToUpdate.forEach((instance) => {
if (idsToExcludeFromUpdate!.has(instance.id)) { if (idsToExcludeFromUpdate!.has(instance.id)) {
@ -327,6 +335,14 @@ const runUpdatePassOnRaf = throttleWithRafFallback(() => {
forceUpdateComponent(instance); forceUpdateComponent(instance);
}); });
const currentLayoutCleanups = pendingLayoutCleanups;
pendingLayoutCleanups = new Map();
currentLayoutCleanups.forEach((cb) => cb());
const currentLayoutEffects = pendingLayoutEffects;
pendingLayoutEffects = new Map();
currentLayoutEffects.forEach((cb) => cb());
}); });
function scheduleUpdate(componentInstance: ComponentInstance) { function scheduleUpdate(componentInstance: ComponentInstance) {
@ -561,9 +577,8 @@ export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter
} }
function useEffectBase( function useEffectBase(
schedulerFn: (cb: NoneToVoidFunction) => void, isLayout: boolean,
primarySchedulerFn: (cb: NoneToVoidFunction) => void, effect: Effect,
effect: () => NoneToVoidFunction | void,
dependencies?: readonly any[], dependencies?: readonly any[],
debugKey?: string, debugKey?: string,
) { ) {
@ -643,8 +658,16 @@ function useEffectBase(
} }
function schedule() { function schedule() {
primarySchedulerFn(execCleanup); const effectId = `${componentInstance.id}_${cursor}`;
schedulerFn(exec);
if (isLayout) {
pendingLayoutCleanups.set(effectId, execCleanup);
pendingLayoutEffects.set(effectId, exec);
} else {
pendingCleanups.set(effectId, execCleanup);
pendingEffects.set(effectId, exec);
runUpdatePassOnRaf();
}
} }
if (dependencies && byCursor[cursor]?.dependencies) { if (dependencies && byCursor[cursor]?.dependencies) {
@ -703,26 +726,12 @@ function useEffectBase(
renderingInstance.hooks.effects.cursor++; renderingInstance.hooks.effects.cursor++;
} }
export function useEffect( export function useEffect(effect: Effect, dependencies?: readonly any[], debugKey?: string) {
effect: () => NoneToVoidFunction | void, return useEffectBase(false, effect, dependencies, debugKey);
dependencies?: readonly any[],
debugKey?: string,
) {
const schedulerFn = useMemo(() => throttleWithRafFallback((cb: NoneToVoidFunction) => cb()), []);
const primarySchedulerFn = useMemo(() => throttleWithPrimaryRafFallback((cb: NoneToVoidFunction) => cb()), []);
return useEffectBase(schedulerFn, primarySchedulerFn, effect, dependencies, debugKey);
} }
export function useLayoutEffect( export function useLayoutEffect(effect: Effect, dependencies?: readonly any[], debugKey?: string) {
effect: () => NoneToVoidFunction | void, return useEffectBase(true, effect, dependencies, debugKey);
dependencies?: readonly any[],
debugKey?: string,
) {
const schedulerFn = useMemo(() => throttleWithTickEnd((cb: NoneToVoidFunction) => cb()), []);
const primarySchedulerFn = useMemo(() => throttleWithPrimaryTickEnd((cb: NoneToVoidFunction) => cb()), []);
return useEffectBase(schedulerFn, primarySchedulerFn, effect, dependencies, debugKey);
} }
export function useMemo<T extends any>(resolver: () => T, dependencies: any[], debugKey?: string): T { export function useMemo<T extends any>(resolver: () => T, dependencies: any[], debugKey?: string): T {

View File

@ -142,7 +142,8 @@ let timeout: NodeJS.Timeout | undefined;
const FAST_RAF_TIMEOUT_FALLBACK_MS = 300; const FAST_RAF_TIMEOUT_FALLBACK_MS = 300;
// May result in an immediate execution if called from another `requestAnimationFrame` callback // May result in an immediate execution if called from another RAF callback which was scheduled
// (and therefore is executed) earlier than RAF callback scheduled by `fastRaf`
export function fastRaf(callback: NoneToVoidFunction, isPrimary = false, withTimeoutFallback = false) { export function fastRaf(callback: NoneToVoidFunction, isPrimary = false, withTimeoutFallback = false) {
if (!fastRafCallbacks) { if (!fastRafCallbacks) {
fastRafCallbacks = !withTimeoutFallback && !isPrimary ? [callback] : []; fastRafCallbacks = !withTimeoutFallback && !isPrimary ? [callback] : [];