diff --git a/.changeset/red-meals-battle.md b/.changeset/red-meals-battle.md new file mode 100644 index 0000000000..c9fd511675 --- /dev/null +++ b/.changeset/red-meals-battle.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": patch +--- + +Fix a problem causing `MainThreadRef`s to not be updated correctly during hydration when they are set to `main-thread:ref`s. diff --git a/packages/react/runtime/__test__/snapshot/workletRef.test.jsx b/packages/react/runtime/__test__/snapshot/workletRef.test.jsx index 6760355fdc..5b9259c940 100644 --- a/packages/react/runtime/__test__/snapshot/workletRef.test.jsx +++ b/packages/react/runtime/__test__/snapshot/workletRef.test.jsx @@ -30,6 +30,11 @@ beforeAll(() => { _runOnBackgroundDelayImpl: { runDelayedBackgroundFunctions: vi.fn(), }, + _eventDelayImpl: { + runDelayedWorklet: vi.fn(), + clearDelayedWorklets: vi.fn(), + }, + _hydrateCtx: vi.fn(), }; globalThis.runWorklet = vi.fn(); }); @@ -334,25 +339,25 @@ describe('WorkletRef', () => { ], [ { - "_execId": 2, "_unmount": undefined, "_wkltId": 233, }, [ - { - "elementRefptr": , - }, + null, ], ], [ { + "_execId": 2, "_unmount": undefined, "_wkltId": 233, }, [ - null, + { + "elementRefptr": , + }, ], ], [ @@ -371,7 +376,7 @@ describe('WorkletRef', () => { ], ] `); - globalThis.runWorklet.mock.calls[1][0]._unmount = cleanup; + globalThis.runWorklet.mock.calls[2][0]._unmount = cleanup; } // update diff --git a/packages/react/runtime/__test__/worklet/workletRef.test.jsx b/packages/react/runtime/__test__/worklet/workletRef.test.jsx index e91f4f84d6..02b2621cc7 100644 --- a/packages/react/runtime/__test__/worklet/workletRef.test.jsx +++ b/packages/react/runtime/__test__/worklet/workletRef.test.jsx @@ -28,6 +28,9 @@ beforeAll(() => { _runOnBackgroundDelayImpl: { runDelayedBackgroundFunctions: vi.fn(), }, + _eventDelayImpl: { + clearDelayedWorklets: vi.fn(), + }, }; }); diff --git a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts index 4d45162913..b2157504ca 100644 --- a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts +++ b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts @@ -6,12 +6,13 @@ import { updateWorkletRefInitValueChanges } from '@lynx-js/react/worklet-runtime import { LifecycleConstant } from '../../lifecycleConstant.js'; import { __pendingListUpdates } from '../../list.js'; -import { markTiming, PerformanceTimingKeys, setPipeline } from '../../lynx/performance.js'; +import { PerformanceTimingKeys, markTiming, setPipeline } from '../../lynx/performance.js'; import { __page } from '../../snapshot.js'; import { getReloadVersion } from '../pass.js'; import type { PatchList, PatchOptions } from './commit.js'; import { setMainThreadHydrationFinished } from './isMainThreadHydrationFinished.js'; import { snapshotPatchApply } from './snapshotPatchApply.js'; +import { applyRefQueue } from '../../snapshot/workletRef.js'; function updateMainThread( { data, patchOptions }: { @@ -46,6 +47,7 @@ function updateMainThread( if (patchOptions.isHydration) { setMainThreadHydrationFinished(true); } + applyRefQueue(); if (patchOptions.pipelineOptions) { flushOptions.pipelineOptions = patchOptions.pipelineOptions; } diff --git a/packages/react/runtime/src/lifecycle/reload.ts b/packages/react/runtime/src/lifecycle/reload.ts index e3aa246df0..c4e6884d9a 100644 --- a/packages/react/runtime/src/lifecycle/reload.ts +++ b/packages/react/runtime/src/lifecycle/reload.ts @@ -13,16 +13,17 @@ import { hydrate } from '../hydrate.js'; import { LifecycleConstant } from '../lifecycleConstant.js'; import { __pendingListUpdates } from '../list.js'; import { __root, setRoot } from '../root.js'; +import { destroyBackground } from './destroy.js'; +import { applyRefQueue } from '../snapshot/workletRef.js'; import { SnapshotInstance, __page, snapshotInstanceManager } from '../snapshot.js'; import { isEmptyObject } from '../utils.js'; -import { destroyBackground } from './destroy.js'; import { destroyWorklet } from '../worklet/destroy.js'; import { clearJSReadyEventIdSwap, isJSReady } from './event/jsReady.js'; import { increaseReloadVersion } from './pass.js'; +import { setMainThreadHydrationFinished } from './patch/isMainThreadHydrationFinished.js'; import { deinitGlobalSnapshotPatch } from './patch/snapshotPatch.js'; import { shouldDelayUiOps } from './ref/delay.js'; import { renderMainThread } from './render.js'; -import { setMainThreadHydrationFinished } from './patch/isMainThreadHydrationFinished.js'; function reloadMainThread(data: any, options: UpdatePageOption): void { if (__PROFILE__) { @@ -51,6 +52,7 @@ function reloadMainThread(data: any, options: UpdatePageOption): void { // always call this before `__FlushElementTree` __pendingListUpdates.flush(); + applyRefQueue(); if (isJSReady) { __OnLifecycleEvent([ diff --git a/packages/react/runtime/src/list.ts b/packages/react/runtime/src/list.ts index fe45e7bf6e..6ec3457efe 100644 --- a/packages/react/runtime/src/list.ts +++ b/packages/react/runtime/src/list.ts @@ -2,6 +2,7 @@ // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. import { hydrate } from './hydrate.js'; +import { applyRefQueue } from './snapshot/workletRef.js'; import type { SnapshotInstance } from './snapshot.js'; export interface ListUpdateInfo { @@ -311,6 +312,7 @@ export function componentAtIndexFactory( hydrate(oldCtx, childCtx); oldCtx.unRenderElements(); const root = childCtx.__element_root!; + applyRefQueue(); if (!enableBatchRender) { const flushOptions: FlushOptions = { triggerLayout: true, @@ -345,6 +347,7 @@ export function componentAtIndexFactory( const root = childCtx.__element_root!; __AppendElement(list, root); const sign = __GetElementUniqueID(root); + applyRefQueue(); if (!enableBatchRender) { __FlushElementTree(root, { triggerLayout: true, diff --git a/packages/react/runtime/src/lynx/calledByNative.ts b/packages/react/runtime/src/lynx/calledByNative.ts index e9082f6ce8..d29f5ed118 100644 --- a/packages/react/runtime/src/lynx/calledByNative.ts +++ b/packages/react/runtime/src/lynx/calledByNative.ts @@ -12,6 +12,7 @@ import { __root, setRoot } from '../root.js'; import { SnapshotInstance, __page, setupPage } from '../snapshot.js'; import { isEmptyObject } from '../utils.js'; import { PerformanceTimingKeys, markTiming, setPipeline } from './performance.js'; +import { applyRefQueue } from '../snapshot/workletRef.js'; function ssrEncode() { const { __opcodes } = __root; @@ -87,6 +88,7 @@ function renderPage(data: any): void { // always call this before `__FlushElementTree` // (There is an implicit `__FlushElementTree` in `renderPage`) __pendingListUpdates.flush(); + applyRefQueue(); if (__FIRST_SCREEN_SYNC_TIMING__ === 'immediately') { jsReady(); @@ -128,6 +130,7 @@ function updatePage(data: any, options?: UpdatePageOption): void { // always call this before `__FlushElementTree` __pendingListUpdates.flush(); + applyRefQueue(); } markTiming(PerformanceTimingKeys.updateDiffVdomEnd); } diff --git a/packages/react/runtime/src/snapshot/workletRef.ts b/packages/react/runtime/src/snapshot/workletRef.ts index 8ee381b74f..bea25209b7 100644 --- a/packages/react/runtime/src/snapshot/workletRef.ts +++ b/packages/react/runtime/src/snapshot/workletRef.ts @@ -1,12 +1,34 @@ // Copyright 2024 The Lynx Authors. All rights reserved. // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. -import { runWorkletCtx, updateWorkletRef as update } from '@lynx-js/react/worklet-runtime/bindings'; + +import { onWorkletCtxUpdate, runWorkletCtx, updateWorkletRef as update } from '@lynx-js/react/worklet-runtime/bindings'; import type { Element, Worklet, WorkletRefImpl } from '@lynx-js/react/worklet-runtime/bindings'; +import { isMainThreadHydrationFinished } from '../lifecycle/patch/isMainThreadHydrationFinished.js'; import type { SnapshotInstance } from '../snapshot.js'; -function workletUnRef(value: Worklet | WorkletRefImpl): void { +let mtRefQueue: (WorkletRefImpl | Worklet | Element)[] = []; + +export function applyRefQueue(): void { + const queue = mtRefQueue; + mtRefQueue = []; + for (let i = 0; i < queue.length; i += 2) { + const worklet = queue[i] as Worklet | WorkletRefImpl; + const element = queue[i + 1] as Element; + if ('_wvid' in worklet) { + update(worklet as WorkletRefImpl, element); + } else if ('_wkltId' in worklet) { + worklet._unmount = runWorkletCtx(worklet, [{ elementRefptr: element }]) as () => void; + } + } +} + +function addToRefQueue(worklet: Worklet | WorkletRefImpl, element: Element): void { + mtRefQueue.push(worklet, element); +} + +export function workletUnRef(value: Worklet | WorkletRefImpl): void { if ('_wvid' in value) { update(value as WorkletRefImpl, null); } else if ('_wkltId' in value) { @@ -18,7 +40,7 @@ function workletUnRef(value: Worklet | WorkletRefImpl): void { } } -function updateWorkletRef( +export function updateWorkletRef( snapshot: SnapshotInstance, expIndex: number, oldValue: WorkletRefImpl | Worklet | undefined, @@ -38,11 +60,17 @@ function updateWorkletRef( if (value === null || value === undefined) { // do nothing } else if (value._wvid) { - update(value as WorkletRefImpl, snapshot.__elements[elementIndex]!); + const element = snapshot.__elements[elementIndex]! as Element; + addToRefQueue(value as Worklet, element); } else if ((value as Worklet)._wkltId) { - (value as Worklet)._unmount = runWorkletCtx(value as Worklet, [{ - elementRefptr: (snapshot.__elements[elementIndex]!) as any, - }]) as () => void; + const element = snapshot.__elements[elementIndex]! as Element; + onWorkletCtxUpdate( + value as Worklet, + oldValue as Worklet | undefined, + !isMainThreadHydrationFinished, + element, + ); + addToRefQueue(value as Worklet, element); /* v8 ignore next 3 */ } else if (value._type === '__LEPUS__' || (value as Worklet)._lepusWorkletHash) { // for pre-0.99 compatibility @@ -58,5 +86,3 @@ function updateWorkletRef( // Add an arbitrary attribute to avoid this element being layout-only __SetAttribute(snapshot.__elements[elementIndex]!, 'has-react-ref', true); } - -export { updateWorkletRef, workletUnRef }; diff --git a/packages/react/worklet-runtime/__test__/workletRef.test.js b/packages/react/worklet-runtime/__test__/workletRef.test.js index b79b1ad1e3..8423bfc0fc 100644 --- a/packages/react/worklet-runtime/__test__/workletRef.test.js +++ b/packages/react/worklet-runtime/__test__/workletRef.test.js @@ -134,11 +134,11 @@ describe('WorkletRef', () => { [4, 'background-thread-init-4'], [5, 'background-thread-init-5'], ]); + globalThis.lynxWorkletImpl._hydrateCtx(worklet, firstScreenWorklet); globalThis.lynxWorkletImpl._refImpl.updateWorkletRef({ _wvid: 5, _initValue: 'background-thread-init-5', }, 'background-thread-element-5'); - globalThis.lynxWorkletImpl._hydrateCtx(worklet, firstScreenWorklet); expect(getFromWorkletRefMap({ _wvid: 1 }).current).toBe('main-thread-set-1'); expect(getFromWorkletRefMap({ _wvid: 2 }).current).toBe('main-thread-init-2'); expect(getFromWorkletRefMap({ _wvid: 3 }).current).toBe('main-thread-init-3'); diff --git a/packages/react/worklet-runtime/src/bindings/observers.ts b/packages/react/worklet-runtime/src/bindings/observers.ts index 7bdde4c561..5f59e2648b 100644 --- a/packages/react/worklet-runtime/src/bindings/observers.ts +++ b/packages/react/worklet-runtime/src/bindings/observers.ts @@ -11,9 +11,8 @@ import type { Worklet } from './types.js'; * @param oldWorklet - The old worklet context * @param isFirstScreen - Whether it is before the hydration is finished * @param element - The element - * @internal */ -function onWorkletCtxUpdate( +export function onWorkletCtxUpdate( worklet: Worklet, oldWorklet: Worklet | null | undefined, isFirstScreen: boolean, @@ -24,17 +23,17 @@ function onWorkletCtxUpdate( globalThis.lynxWorkletImpl?._hydrateCtx(worklet, oldWorklet); } // For old version dynamic component compatibility. - globalThis.lynxWorkletImpl?._eventDelayImpl.runDelayedWorklet(worklet, element); + if (isFirstScreen) { + globalThis.lynxWorkletImpl?._eventDelayImpl.runDelayedWorklet(worklet, element); + } } /** * This must be called when the hydration is finished. - * - * @internal */ -function onHydrationFinished(): void { +export function onHydrationFinished(): void { globalThis.lynxWorkletImpl?._runOnBackgroundDelayImpl.runDelayedBackgroundFunctions(); globalThis.lynxWorkletImpl?._refImpl.clearFirstScreenWorkletRefMap(); + // For old version dynamic component compatibility. + globalThis.lynxWorkletImpl?._eventDelayImpl.clearDelayedWorklets(); } - -export { onWorkletCtxUpdate, onHydrationFinished }; diff --git a/packages/react/worklet-runtime/src/hydrate.ts b/packages/react/worklet-runtime/src/hydrate.ts index 45297d478b..3fd7a75d9f 100644 --- a/packages/react/worklet-runtime/src/hydrate.ts +++ b/packages/react/worklet-runtime/src/hydrate.ts @@ -2,7 +2,6 @@ // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. -import { Element } from './api/element.js'; import type { ClosureValueType, JsFnHandle, Worklet, WorkletRefId, WorkletRefImpl } from './bindings/index.js'; import { profile } from './utils/profile.js'; @@ -64,10 +63,6 @@ function hydrateMainThreadRef(refId: WorkletRefId, value: WorkletRefImpl