diff --git a/mangle.json b/mangle.json index b5127c0b6..a44ad851d 100644 --- a/mangle.json +++ b/mangle.json @@ -28,6 +28,15 @@ "props": { "cname": 6, "props": { + "$__persistentState": "__$p", + "$_list": "__", + "$_pendingSetup": "__h", + "$_cleanup": "__c", + "$_stateValue": "__", + "$_args": "__H", + "$_stored": "__s", + "$_renderCallbacks": "__h", + "$_skipEffects": "__s", "core: Node": "", "$_watched": "W", "$_unwatched": "Z", diff --git a/packages/preact/src/index.ts b/packages/preact/src/index.ts index 11f124c51..480ec33ca 100644 --- a/packages/preact/src/index.ts +++ b/packages/preact/src/index.ts @@ -1,5 +1,4 @@ import { options, Component, isValidElement, Fragment } from "preact"; -import { useRef, useMemo, useEffect } from "preact/hooks"; import { signal, computed, @@ -18,6 +17,7 @@ import { PropertyUpdater, AugmentedComponent, AugmentedElement as Element, + HookState, } from "./internal"; export { @@ -53,6 +53,7 @@ function hook(hookName: T, hookFn: HookFn) { let currentComponent: AugmentedComponent | undefined; let finishUpdate: (() => void) | undefined; +let setupTasks: AugmentedComponent[] = []; function setCurrentUpdater(updater?: Effect) { // end tracking for the current update: @@ -91,7 +92,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) { const currentSignal = useSignal(data); currentSignal.value = data; - const [isText, s] = useMemo(() => { + const [isText, s] = useStoreValueOnce(() => { let self = this; // mark the parent component as having computeds so it gets optimized let v = this.__v; @@ -138,7 +139,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) { }; return [isText, wrappedSignal]; - }, []); + }); // Rerender the component whenever `data.value` changes from a VNode // to another VNode, from text to a VNode, or from a VNode to text. @@ -210,6 +211,7 @@ hook(OptionsTypes.RENDER, (old, vnode) => { } } + currentHookIndex = 0; currentComponent = component; setCurrentUpdater(updater); } @@ -262,7 +264,17 @@ hook(OptionsTypes.DIFFED, (old, vnode) => { } } } + } else if (vnode.__c) { + let component = vnode.__c as AugmentedComponent; + if ( + component.__persistentState && + component.__persistentState._pendingSetup.length + ) { + console.log("f", component.__persistentState._pendingSetup); + queueSetupTasks(setupTasks.push(component)); + } } + old(vnode); }); @@ -326,8 +338,15 @@ hook(OptionsTypes.UNMOUNT, (old, vnode: VNode) => { component._updater = undefined; updater._dispose(); } + + const persistentState = component.__persistentState; + if (persistentState) { + // Cleanup all the stored effects + persistentState._list.forEach(invokeCleanup); + } } } + old(vnode); }); @@ -386,9 +405,8 @@ Component.prototype.shouldComponentUpdate = function ( export function useSignal(value: T, options?: SignalOptions): Signal; export function useSignal(): Signal; export function useSignal(value?: T, options?: SignalOptions) { - return useMemo( - () => signal(value, options as SignalOptions), - [] + return useStoreValueOnce(() => + signal(value, options as SignalOptions) ); } @@ -396,7 +414,9 @@ export function useComputed(compute: () => T, options?: SignalOptions) { const $compute = useRef(compute); $compute.current = compute; (currentComponent as AugmentedComponent)._updateFlags |= HAS_COMPUTEDS; - return useMemo(() => computed(() => $compute.current(), options), []); + return useStoreValueOnce(() => + computed(() => $compute.current(), options) + ); } function safeRaf(callback: () => void) { @@ -434,6 +454,32 @@ function notifyEffects(this: Effect) { } } +let prevRaf: typeof options.requestAnimationFrame | undefined; +function queueSetupTasks(newQueueLength: number) { + if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) { + prevRaf = options.requestAnimationFrame; + (prevRaf || deferEffects)(flushSetup); + } +} + +/** + * After paint effects consumer. + */ +function flushSetup() { + let component; + while ((component = setupTasks.shift())) { + console.log("flushSetup", component.__P, component.__persistentState); + if (!component.__persistentState || !component.__P) continue; + try { + component.__persistentState._pendingSetup.forEach(invokeCleanup); + component.__persistentState._pendingSetup.forEach(invokeEffect); + component.__persistentState._pendingSetup = []; + } catch (e) { + component.__persistentState._pendingSetup = []; + } + } +} + function flushDomUpdates() { batch(() => { let inst: Effect | undefined; @@ -453,78 +499,66 @@ export function useSignalEffect(cb: () => void | (() => void)) { const callback = useRef(cb); callback.current = cb; - useEffect(() => { + useOnce(() => { return effect(function (this: Effect) { this._notify = notifyEffects; return callback.current(); }); - }, []); + }); } -/** - * @todo Determine which Reactive implementation we'll be using. - * @internal - */ -// export function useReactive(value: T): Reactive { -// return useMemo(() => reactive(value), []); -// } +let currentHookIndex = 0; +function getState(index: number): HookState { + if (!currentComponent) { + throw new Error("Hooks can only be called inside components"); + } -/** - * @internal - * Update a Reactive's using the properties of an object or other Reactive. - * Also works for Signals. - * @example - * // Update a Reactive with Object.assign()-like syntax: - * const r = reactive({ name: "Alice" }); - * update(r, { name: "Bob" }); - * update(r, { age: 42 }); // property 'age' does not exist in type '{ name?: string }' - * update(r, 2); // '2' has no properties in common with '{ name?: string }' - * console.log(r.name.value); // "Bob" - * - * @example - * // Update a Reactive with the properties of another Reactive: - * const A = reactive({ name: "Alice" }); - * const B = reactive({ name: "Bob", age: 42 }); - * update(A, B); - * console.log(`${A.name} is ${A.age}`); // "Bob is 42" - * - * @example - * // Update a signal with assign()-like syntax: - * const s = signal(42); - * update(s, "hi"); // Argument type 'string' not assignable to type 'number' - * update(s, {}); // Argument type '{}' not assignable to type 'number' - * update(s, 43); - * console.log(s.value); // 43 - * - * @param obj The Reactive or Signal to be updated - * @param update The value, Signal, object or Reactive to update `obj` to match - * @param overwrite If `true`, any properties `obj` missing from `update` are set to `undefined` - */ -/* -export function update( - obj: T, - update: Partial>, - overwrite = false -) { - if (obj instanceof Signal) { - obj.value = peekValue(update); - } else { - for (let i in update) { - if (i in obj) { - obj[i].value = peekValue(update[i]); - } else { - let sig = signal(peekValue(update[i])); - sig[KEY] = i; - obj[i] = sig; - } - } - if (overwrite) { - for (let i in obj) { - if (!(i in update)) { - obj[i].value = undefined; - } - } - } + const hooks = + currentComponent.__persistentState || + (currentComponent.__persistentState = { + _list: [], + _pendingSetup: [], + }); + + if (index >= hooks._list.length) { + hooks._list.push({}); + } + + return hooks._list[index]; +} + +export function useStoreValueOnce(factory: () => T): T { + const state = getState(currentHookIndex++); + if (!state._stored || (options as any)._skipEffects) { + state._stored = true; + state._stateValue = factory(); + } + return state._stateValue; +} + +export function useRef(initialValue: T): { current: T } { + return useStoreValueOnce(() => ({ current: initialValue })); +} + +function useOnce(callback: () => void | (() => void)): void { + const state = getState(currentHookIndex++); + if (!state._executed) { + state._executed = true; + state._stateValue = callback; + currentComponent!.__persistentState._pendingSetup.push(state); + } +} + +function invokeEffect(hook: HookState): void { + console.log("invokeEffect", hook); + if (hook._stateValue) { + hook._cleanup = hook._stateValue() || undefined; + } +} + +function invokeCleanup(hook: HookState): void { + if (hook._cleanup) { + hook._cleanup(); + hook._cleanup = undefined; } } -*/ diff --git a/packages/preact/src/internal.d.ts b/packages/preact/src/internal.d.ts index 19912ecf8..d0a7ebb5c 100644 --- a/packages/preact/src/internal.d.ts +++ b/packages/preact/src/internal.d.ts @@ -27,6 +27,20 @@ export interface AugmentedComponent extends Component { __v: VNode; _updater?: Effect; _updateFlags: number; + __persistentState: SignalState; + __P?: Element | Text | null; +} + +export interface HookState { + _executed?: boolean; + _stored?: boolean; + _stateValue?: any; + _cleanup?: () => void; +} + +export interface SignalState { + _list: HookState[]; + _pendingSetup: HookState[]; } export interface VNode

extends preact.VNode

{ diff --git a/packages/preact/utils/src/index.ts b/packages/preact/utils/src/index.ts index 785d447b9..84483fbb1 100644 --- a/packages/preact/utils/src/index.ts +++ b/packages/preact/utils/src/index.ts @@ -1,7 +1,6 @@ import { ReadonlySignal, Signal } from "@preact/signals-core"; -import { useSignal } from "@preact/signals"; +import { useSignal, useStoreValueOnce } from "@preact/signals"; import { Fragment, createElement, JSX } from "preact"; -import { useMemo } from "preact/hooks"; interface ShowProps { when: Signal | ReadonlySignal; @@ -27,7 +26,7 @@ interface ForProps { } export function For(props: ForProps): JSX.Element | null { - const cache = useMemo(() => new Map(), []); + const cache = useStoreValueOnce(() => new Map()); let list = ( (typeof props.each === "function" ? props.each() : props.each) as Signal< Array @@ -60,6 +59,7 @@ export function useSignalRef(value: T): Signal & { current: T } { Object.defineProperty(ref, "current", refSignalProto); return ref; } + const refSignalProto = { configurable: true, get(this: Signal) {