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