diff --git a/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js b/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js index 839ecc7b81348c4fdd86aa26344c5f8c205a0dbd..d9eda3401b5167d49208debd1c3225b8eff432c4 100644 --- a/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js +++ b/frameworks/bridge/declarative_frontend/engine/jsStateManagement.js @@ -294,6 +294,46 @@ class UIUtils { static clearMonitor(target, path, monitorCallback) { UIUtils.uiUtilsImpl_.clearMonitor(target, path, monitorCallback) ; } + + /** + * Runs a task and processes all resulting updates immediately + * + * @param { object } task to be executed + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @crossplatform + * @atomicservice + * @returns { T } The value returned by the task + */ + static applySync(task) { + return UIUtils.uiUtilsImpl_.applySync(task); + } + + /** + * Immediately processes all updates + * + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @crossplatform + * @atomicservice + */ + static flushUpdates() { + return UIUtils.uiUtilsImpl_.flushUpdates(); + } + + + /** + * Immediately rerenders UINodes that need updates + * + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @crossplatform + * @atomicservice + */ + static flushUiUpdates() { + return UIUtils.uiUtilsImpl_.flushUiUpdates(); + } + } UIUtils.uiUtilsImpl_ = UIUtilsImpl.instance(); diff --git a/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp b/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp index 0dd7b33db087149007df7f2e1a1772ab5544219a..6b08ea2baff24e989d4b928dc09ef667fa0bb6f9 100644 --- a/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp +++ b/frameworks/bridge/declarative_frontend/jsview/js_view_context.cpp @@ -30,6 +30,7 @@ #include "bridge/declarative_frontend/engine/functions/js_animation_on_finish_function.h" #include "bridge/declarative_frontend/engine/js_converter.h" #include "bridge/declarative_frontend/jsview/js_view_abstract.h" +#include "bridge/declarative_frontend/jsview/js_view_common_def.h" #include "bridge/declarative_frontend/jsview/js_tabs_feature.h" #include "bridge/declarative_frontend/jsview/models/view_context_model_impl.h" #include "core/animation/animation_pub.h" @@ -791,6 +792,7 @@ void JSViewContext::AnimateToInner(const JSCallbackInfo& info, bool immediately) TAG_LOGW(AceLogTag::ACE_FORM, "[Form animation] Form animation SetDuration: %{public}lld ms", static_cast(DEFAULT_DURATION - GetFormAnimationTimeInterval(pipelineContext))); } + if (SystemProperties::GetRosenBackendEnabled()) { bool usingSharedRuntime = container->GetSettings().usingSharedRuntime; if (usingSharedRuntime) { diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts index 44c49328c23576d5c2bbd8733228b7f63adfecfd..18e41b8fe81d8f9051ec3dd46d2c2b15ed69dfba 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/ui_utils.ts @@ -97,6 +97,18 @@ class UIUtilsImpl { ObserveV2.getObserve().clearMonitorPath(target, path, monitorFunc); } + public static applySync(task : () => T) : T { + return ObserveV2.getObserve().applySync(task); + } + + public static flushUpdates() { + ObserveV2.getObserve().flushUpdates(); + } + + public static flushUiUpdates() { + ObserveV2.getObserve().flushUiUpdates(); + } + public static instance(): UIUtilsImpl { if (UIUtilsImpl.instance_) { return UIUtilsImpl.instance_; diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts index f8acc2f806073354def19625032e175b6e3ec1ea..a1be8af0f442052773170944d5fe3edf109b6eb7 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_change_observation.ts @@ -141,6 +141,9 @@ class ObserveV2 { // flag to disable nested component optimization if V1 and V2 components are involved in the nested cases. public isParentChildOptimizable_ : boolean = true; + // counter for nested applySync() calls + private applySyncRunningCount_: number = 0; + private static obsInstance_: ObserveV2; public static getObserve(): ObserveV2 { @@ -490,8 +493,7 @@ class ObserveV2 { return ret; } - - + /** * mark view model object 'target' property 'attrName' as changed * notify affected watchIds and elmtIds but exclude given elmtIds @@ -756,10 +758,11 @@ class ObserveV2 { * process UINode update until no more @Computed and @Monitor and UINode rerender * * @param updateUISynchronously should be set to true if called during VSYNC only + * @param snapshot optional, used by applySync() to track processed IDs for optimization * */ - public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): void { + public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false, snapshot?: Object) { aceDebugTrace.begin('updateDirty2'); stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} ... `); // obtain and unregister the removed elmtIds @@ -772,10 +775,14 @@ class ObserveV2 { // 3- update UINodes until no more monitors, no more computed props, and no more UINodes // FIXME prevent infinite loops do { - this.updateComputedAndMonitors(); + // update and remove processed ids from the snapshot + this.updateComputedAndMonitors(snapshot); if (this.elmtIdsChanged_.size) { const elmtIds = Array.from(this.elmtIdsChanged_).sort((elmtId1, elmtId2) => elmtId1 - elmtId2); + if (snapshot) { + this.elmtIdsChanged_.forEach(Set.prototype.delete, snapshot['elmtIdsChanged_']); + } this.elmtIdsChanged_ = new Set(); updateUISynchronously ? isReuse ? this.updateUINodesForReuse(elmtIds) : this.updateUINodesSynchronously(elmtIds) : this.updateUINodes(elmtIds); } @@ -793,26 +800,37 @@ class ObserveV2 { * three nested loops, means: * process @Computed until no more @Computed need update * process @Monitor until no more @Computed and @Monitor + * + * @param snapshot optional, used by applySync() to track processed IDs for optimization */ - private updateComputedAndMonitors(): void { + private updateComputedAndMonitors(snapshot?: Object) { do { while (this.computedPropIdsChanged_.size) { // sort the ids and update in ascending order // If a @Computed property depends on other @Computed properties, their // ids will be smaller as they are defined first. const computedProps = Array.from(this.computedPropIdsChanged_).sort((id1, id2) => id1 - id2); + if (snapshot) { + this.computedPropIdsChanged_.forEach(Set.prototype.delete, snapshot['computedPropIdsChanged_']); + } this.computedPropIdsChanged_ = new Set(); this.updateDirtyComputedProps(computedProps); } if (this.persistenceChanged_.size) { const persistKeys: Array = Array.from(this.persistenceChanged_); + if (snapshot) { + this.persistenceChanged_.forEach(Set.prototype.delete, snapshot['persistenceChanged_']); + } this.persistenceChanged_ = new Set(); PersistenceV2Impl.instance().onChangeObserved(persistKeys); } if (this.monitorIdsChanged_.size) { const monitors = this.monitorIdsChanged_; + if (snapshot) { + this.monitorIdsChanged_.forEach(Set.prototype.delete, snapshot['monitorIdsChanged_']); + } this.monitorIdsChanged_ = new Set(); this.updateDirtyMonitors(monitors); } @@ -894,7 +912,7 @@ class ObserveV2 { // monitor notifyChange delayed if target is a View that is not active monitorTarget.addDelayedMonitorIds(watchId); } else { - monitor.notifyChange(); + monitor.notifyChange(); // can delete this.elmtIdsChanged_ } } }); @@ -1317,6 +1335,71 @@ class ObserveV2 { public setCurrentReuseId(elmtId: number): void { this.currentReuseId_ = elmtId; } + + // Runs a task and processes all resulting updates immediately + public applySync(task: () => T) : T { + if (ComputedV2.runningCount) { + throw new Error('applySync() is not allowed inside @Computed'); + } + + this.applySyncRunningCount_++; + + const names = [ + 'elmtIdsChanged_', + 'computedPropIdsChanged_', + 'monitorIdsChanged_', 'monitorIdsChangedForAddMonitor_', 'monitorSyncIdsChangedForAddMonitor_', + 'persistenceChanged_' + ]; + + // Take a snapshot of the current pending IDs and reset them to empty sets + const snapshot = {}; + names.forEach(name => snapshot[name] = this[name]); + names.forEach(name => this[name] = new Set()); + + // Execute the task — any state changes here will be recorded in the cleared sets + const retValue = task(); + + // Remove IDs added by task() from the original snapshot + names.forEach((name) => + this[name].forEach(Set.prototype.delete, snapshot[name]) + ); + + // Process the new changes immediately (may produce further changes) and + // remove IDs added by updateDirty2 from the original snapshot + this.updateDirty2(false, false, snapshot); + + // Restore unprocessed IDs from the snapshot + names.forEach((name) => this[name] = snapshot[name]); + + this.applySyncRunningCount_--; + return retValue; + } + + // Immediately processes all updates + public flushUpdates() { + if (ComputedV2.runningCount || MonitorV2.runningCount) { + throw new Error('flushUpdates() is not allowed inside @Computed and @Monitor'); + } + if (this.applySyncRunningCount_) { + stateMgmtConsole.log('flushUpdates() skipped inside applySync()'); + return; + } + this.updateDirty2(false); + } + + // Update all UINodes that currently need update + public flushUiUpdates() { + if (ComputedV2.runningCount || MonitorV2.runningCount) { + throw new Error('flushUiUpdates() is not allowed inside @Computed and @Monitor'); + } + if (this.applySyncRunningCount_) { + stateMgmtConsole.log('flushUiUpdates() skipped inside applySync()'); + return; + } + const elmtIds = Array.from(this.elmtIdsChanged_).sort((id1, id2) => id1 - id2); + this.updateUINodes(elmtIds); + } + } // class ObserveV2 const trackInternal = ( @@ -1351,7 +1434,11 @@ const trackInternal = ( target[ObserveV2.V2_DECO_META] ??= {}; }; // trackInternal -// used to manually mark dirty v2 before animateTo -function __updateDirty2Immediately_V2_Change_Observation(): void { - ObserveV2.getObserve().updateDirty2(); -} \ No newline at end of file +// Override animateTo() to run its callback via ObserveV2.applySync() +(() => { + const animateTo = globalThis.Context.animateTo; + globalThis.Context.animateTo = (value: object, event: () => void) => + animateTo.call(globalThis.Context, value, () => { + ObserveV2.getObserve().applySync(event) + }); +})(); diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts index e176fde0a60cf979abfcd2de5e8817c1fa7b8e48..c093edef52492560768532e8795c51c9be485fee 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_computed.ts @@ -46,6 +46,8 @@ class ComputedV2 { public static readonly COMPUTED_PREFIX = '___comp_'; public static readonly COMPUTED_CACHED_PREFIX = '___comp_cached_'; + // count of currently running @Computed functions + public static runningCount = 0; constructor(target: object, prop: string, func: (...args: any[]) => any) { this.target_ = target; @@ -105,6 +107,7 @@ class ComputedV2 { let ret; try { + ComputedV2.runningCount++; ret = this.propertyComputeFunc_.call(this.target_); } catch (e) { stateMgmtConsole.applicationError(`@Computed Exception caught for ${this.propertyComputeFunc_.name}`, e.toString()); @@ -112,6 +115,7 @@ class ComputedV2 { throw e; } finally { ObserveV2.getObserve().stopRecordDependencies(); + ComputedV2.runningCount--; } return ret; diff --git a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts index 0e174d71435f20d810bbd89b64fa3dc6d7b8426e..5109a4c6a0e9690c35119c7fbbc94eab34ab24bc 100644 --- a/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts +++ b/frameworks/bridge/declarative_frontend/state_mgmt/src/lib/v2/v2_monitor.ts @@ -109,6 +109,8 @@ class MonitorV2 { public static nextWatchId_ = MonitorV2.MIN_WATCH_ID; public static nextWatchApiId_ = MonitorV2.MIN_WATCH_FROM_API_ID; public static nextSyncWatchApiId_ = MonitorV2.MIN_SYNC_WATCH_FROM_API_ID; + // count of currently running @Monitor functions + public static runningCount = 0; private values_: Map> = new Map>(); private target_: object; // @Monitor function 'this': data object or ViewV2 @@ -246,18 +248,21 @@ class MonitorV2 { // Executes the monitor function. public runMonitorFunction(): void { stateMgmtConsole.debug(`@Monitor function '${this.monitorFunction.name}' exec ...`); + if (this.dirty.length === 0) { stateMgmtConsole.debug(`No dirty values! not firing!`); return; } try { // exec @Monitor/AddMonitor function + MonitorV2.runningCount++; this.monitorFunction.call(this.target_, this); } catch (e) { stateMgmtConsole.applicationError(`AddMonitor exception caught for ${this.monitorFunction.name}`, e.toString()); throw e; } finally { this.resetMonitor(); + MonitorV2.runningCount--; } }