Teact: Avoid multiple effects in a single frame

This commit is contained in:
Alexander Zinchuk 2023-02-08 00:43:29 +01:00
parent 31e52956c2
commit d40a2a6240
2 changed files with 43 additions and 24 deletions

View File

@ -1,11 +1,10 @@
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { DEBUG, DEBUG_MORE } from '../../config'; import { DEBUG, DEBUG_MORE } from '../../config';
import { import {
fastRafWithFallback,
fastRafPrimaryWithFallback,
onTickEnd,
onTickEndPrimary,
throttleWithRafFallback, throttleWithRafFallback,
throttleWithPrimaryRafFallback,
throttleWithTickEnd,
throttleWithPrimaryTickEnd,
} from '../../util/schedulers'; } from '../../util/schedulers';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
import { getUnequalProps } from '../../util/arePropsShallowEqual'; import { getUnequalProps } from '../../util/arePropsShallowEqual';
@ -15,7 +14,9 @@ import { incrementOverlayCounter } from '../../util/debugOverlay';
export type Props = AnyLiteral; export type Props = AnyLiteral;
export type FC<P extends Props = any> = (props: P) => any; export type FC<P extends Props = any> = (props: P) => any;
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
export type FC_withDebug = FC & { DEBUG_contentComponentName?: string }; export type FC_withDebug =
FC
& { DEBUG_contentComponentName?: string };
export enum VirtualElementTypesEnum { export enum VirtualElementTypesEnum {
Empty, Empty,
@ -79,9 +80,8 @@ interface ComponentInstance {
effects: { effects: {
cursor: number; cursor: number;
byCursor: { byCursor: {
effect: () => void;
dependencies?: readonly any[]; dependencies?: readonly any[];
cleanup?: Function; cleanup?: NoneToVoidFunction;
}[]; }[];
}; };
memos: { memos: {
@ -455,8 +455,7 @@ export function unmountComponent(componentInstance: ComponentInstance) {
// We need to remove all references to DOM objects. We also clean all other references, just in case // We need to remove all references to DOM objects. We also clean all other references, just in case
function helpGc(componentInstance: ComponentInstance) { function helpGc(componentInstance: ComponentInstance) {
componentInstance.hooks.effects.byCursor.forEach((hook) => { componentInstance.hooks.effects.byCursor.forEach((hook) => {
hook.cleanup = undefined; hook.cleanup = undefined as any;
hook.effect = undefined as any;
hook.dependencies = undefined; hook.dependencies = undefined;
}); });
@ -554,10 +553,10 @@ export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter
]; ];
} }
function useLayoutEffectBase( function useEffectBase(
schedulerFn: typeof onTickEnd | typeof requestAnimationFrame, schedulerFn: (cb: NoneToVoidFunction) => void,
primarySchedulerFn: typeof onTickEnd | typeof requestAnimationFrame, primarySchedulerFn: (cb: NoneToVoidFunction) => void,
effect: () => Function | void, effect: () => NoneToVoidFunction | void,
dependencies?: readonly any[], dependencies?: readonly any[],
debugKey?: string, debugKey?: string,
) { ) {
@ -636,7 +635,12 @@ function useLayoutEffectBase(
} }
} }
if (byCursor[cursor] !== undefined && dependencies && byCursor[cursor].dependencies) { function schedule() {
primarySchedulerFn(execCleanup);
schedulerFn(exec);
}
if (dependencies && byCursor[cursor]?.dependencies) {
if (dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies![i])) { if (dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies![i])) {
if (debugKey) { if (debugKey) {
const causedBy = dependencies.reduce((res, newValue, i) => { const causedBy = dependencies.reduce((res, newValue, i) => {
@ -652,8 +656,7 @@ function useLayoutEffectBase(
console.log(`[Teact] Effect "${debugKey}" caused by dependencies.`, causedBy.join(', ')); console.log(`[Teact] Effect "${debugKey}" caused by dependencies.`, causedBy.join(', '));
} }
primarySchedulerFn(execCleanup); schedule();
schedulerFn(exec);
} }
} else { } else {
if (debugKey) { if (debugKey) {
@ -661,25 +664,37 @@ function useLayoutEffectBase(
console.log(`[Teact] Effect "${debugKey}" caused by missing dependencies.`); console.log(`[Teact] Effect "${debugKey}" caused by missing dependencies.`);
} }
primarySchedulerFn(execCleanup); schedule();
schedulerFn(exec);
} }
byCursor[cursor] = { byCursor[cursor] = {
effect, ...byCursor[cursor],
dependencies, dependencies,
cleanup: byCursor[cursor]?.cleanup,
}; };
renderingInstance.hooks.effects.cursor++; renderingInstance.hooks.effects.cursor++;
} }
export function useEffect(effect: () => Function | void, dependencies?: readonly any[], debugKey?: string) { export function useEffect(
return useLayoutEffectBase(fastRafWithFallback, fastRafPrimaryWithFallback, effect, dependencies, debugKey); effect: () => NoneToVoidFunction | void,
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(effect: () => Function | void, dependencies?: readonly any[], debugKey?: string) { export function useLayoutEffect(
return useLayoutEffectBase(onTickEnd, onTickEndPrimary, effect, dependencies, debugKey); effect: () => NoneToVoidFunction | void,
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

@ -92,6 +92,10 @@ export function throttleWithTickEnd<F extends AnyToVoidFunction>(fn: F) {
return throttleWith(onTickEnd, fn); return throttleWith(onTickEnd, fn);
} }
export function throttleWithPrimaryTickEnd<F extends AnyToVoidFunction>(fn: F) {
return throttleWith(onTickEndPrimary, fn);
}
export function throttleWith<F extends AnyToVoidFunction>(schedulerFn: Scheduler, fn: F) { export function throttleWith<F extends AnyToVoidFunction>(schedulerFn: Scheduler, fn: F) {
let waiting = false; let waiting = false;
let args: Parameters<F>; let args: Parameters<F>;