已开启
fix:处理Screen转场周期事件误发导致周期回调异常的问题 #62
fix:处理Screen转场周期事件误发导致周期回调异常的问题 #62
已开启
yanPeng创建于 5月28日
3 个文件变更+349-76
@@ -207,6 +207,21 @@ export struct RNSScreen {
207 return this.currentOffset207 return this.currentOffset
208 }208 }
209 209 
210+ private getStackParentTag(): number | undefined {
211+ if (this.parentTag !== undefined) {
212+ return this.parentTag;
213+ }
214+ return this.ctx.descriptorRegistry.getDescriptor(this.tag)?.parentTag;
215+ }
216+ 
217+ private shouldEmitTransitionLifecycleToJs(): boolean {
218+ const stackTag = this.getStackParentTag();
219+ if (stackTag === undefined) {
220+ return true;
221+ }
222+ return RNSScreenStack.shouldEmitScreenTransitionLifecycle(stackTag, this.tag);
223+ }
224+ 
210 private isPreventKeyboardPopUp: boolean = false;225 private isPreventKeyboardPopUp: boolean = false;
211 226 
212 getOffsetHeihtByTag(h: number): number {227 getOffsetHeihtByTag(h: number): number {
@@ -319,7 +334,7 @@ export struct RNSScreen {
319 // presentation 值为modal、transparentModal334 // presentation 值为modal、transparentModal
320 // cardStyleInterpolator 值为forModalPresentationIOS335 // cardStyleInterpolator 值为forModalPresentationIOS
321 // 时非首页页面按钮点击无效问题处理336 // 时非首页页面按钮点击无效问题处理
322- this.nativeStackJsTarge = args[0];337+ this.nativeStackJsTarge = args[0];
323 this.nativeStackJsData = args[1];338 this.nativeStackJsData = args[1];
324 return;339 return;
325 }340 }
@@ -751,7 +766,7 @@ export struct RNSScreen {
751 private isCustomAnimation(): boolean {766 private isCustomAnimation(): boolean {
752 return (this.stackAnimation != "flip" && this.stackAnimation != "default");767 return (this.stackAnimation != "flip" && this.stackAnimation != "default");
753 }768 }
754- 769+ 
755 private isRootStackGesturePopDisallowed(): boolean {770 private isRootStackGesturePopDisallowed(): boolean {
756 const nativeLen: number = this.stackController?.getAllPathName()?.length ?? 0;771 const nativeLen: number = this.stackController?.getAllPathName()?.length ?? 0;
757 return nativeLen <= 1;772 return nativeLen <= 1;
@@ -1095,10 +1110,14 @@ export struct RNSScreen {
1095 }1110 }
1096 })1111 })
1097 .onWillAppear(() => {1112 .onWillAppear(() => {
1098- this.eventEmitter!.emit("willAppear", {})1113+ if (this.shouldEmitTransitionLifecycleToJs()) {
1114+ this.eventEmitter!.emit("willAppear", {})
1115+ }
1099 })1116 })
1100 .onAppear(() => {1117 .onAppear(() => {
1101- this.eventEmitter!.emit("appear", {})1118+ if (this.shouldEmitTransitionLifecycleToJs()) {
1119+ this.eventEmitter!.emit("appear", {})
1120+ }
1102 })1121 })
1103 .onShown(async () => {1122 .onShown(async () => {
cpf-manager
cpf-managercpf-manager5月31日

【AI-Review】【严重】【基础代码问题】【稳定性问题】onShown回调未受Gate保护,NavPathStack重排时可能污染全局导航状态

● 问题:PR对onWillAppearonAppearonWillDisappearonDisAppear四个生命周期回调都加了shouldEmitTransitionLifecycleToJs()守卫,但onShown回调(第1122行)未做同样的守卫检查。onShown中更新了RNSScreen.TOP_PAGEIDRNSScreen.TOP_PAGEID_ARRRNSScreen.TOP_PAGE_PRESENTATION_MAPRNSScreen.TOP_PAGE_PREVENT_MAPRNSScreen.TOP_PAGE_TAG_MAP等全局静态状态。

当NavPathStack发生重排(如stackUpadatesortScreens)时,ArkUI Navigation框架可能触发中间屏幕的onShown回调。此时非栈顶屏幕的onShown会将TOP_PAGEID设置为错误的pageId,导致:

  1. 后续onBackPressed回退到错误的屏幕
  2. isFirstScreenInStack判断错误
  3. Modal/dismiss行为异常(因为TOP_PAGE_PRESENTATION_MAPTOP_PAGE_PREVENT_MAP被错误覆盖)

● 影响:严重。在NavPathStack重排场景下,全局导航状态被中间屏幕污染,导致后续导航操作(返回、dismiss等)指向错误目标,可能造成页面卡死或闪退。

● 建议:对onShown中的全局状态更新也加入shouldEmitTransitionLifecycleToJs()守卫,仅允许真正的转场参与屏幕更新全局导航状态:

.onShown(async () => {
  if (this.shouldEmitTransitionLifecycleToJs()) {
    RNSScreen.TOP_PAGEID = this.pageId;
    if(!RNSScreen.TOP_PAGEID_ARR.some((num)=> num === this.pageId)) {
      RNSScreen.TOP_PAGEID_ARR.push(this.pageId)
    } else {
      let arr = RNSScreen.TOP_PAGEID_ARR;
      RNSScreen.TOP_PAGEID_ARR = arr.slice(0, arr.indexOf(this.pageId)+1)
    }
    RNSScreen.TOP_PAGE_PRESENTATION_MAP.set(this.pageId, this.stackPresentationMode)
    RNSScreen.TOP_PAGE_PREVENT_MAP.set(this.pageId, this.preventNativeDismiss)
    RNSScreen.TOP_PAGE_TAG_MAP.set(this.pageId, this.tag)
  }
  // 屏幕自身的状态更新可保留,不受Gate影响
  await this.updateState(true);
  await this.updateHeaderConfigState();
  this.setScreenOrientation()
  // ...
})

注意:Gate清理(scheduleClear)触发后,被抑制的全局状态不会自动补偿。如果此场景存在,需在scheduleClear回调中补充对当前真实栈顶屏幕的状态刷新逻辑。

likedislike
1104 RNSScreen.TOP_PAGEID = this.pageId;1123 RNSScreen.TOP_PAGEID = this.pageId;
@@ -1131,14 +1150,18 @@ export struct RNSScreen {
1131 });1150 });
1132 })1151 })
1133 .onWillDisappear(() => {1152 .onWillDisappear(() => {
1134- this.eventEmitter!.emit("willDisappear", {})1153+ if (this.shouldEmitTransitionLifecycleToJs()) {
1154+ this.eventEmitter!.emit("willDisappear", {})
1155+ }
1135 })1156 })
1136 .onDisAppear(() => {1157 .onDisAppear(() => {
1137 this.logger.debug('onDisAppear ' + this.pageId + ' , screenId: ' + this.descriptor.rawProps["screenId"])1158 this.logger.debug('onDisAppear ' + this.pageId + ' , screenId: ' + this.descriptor.rawProps["screenId"])
1138 this.releaseScreenOrientation();1159 this.releaseScreenOrientation();
1139 // 延后 unregister:replace/pop 转场期间仍依赖 CustomTransitionCoordinator 中离场页的 AnimateCallback1160 // 延后 unregister:replace/pop 转场期间仍依赖 CustomTransitionCoordinator 中离场页的 AnimateCallback
1140 this.customTransitionCoordinator?.scheduleDeferredUnregister(this.pageId);1161 this.customTransitionCoordinator?.scheduleDeferredUnregister(this.pageId);
1141- this.eventEmitter!.emit("disappear", {})1162+ if (this.shouldEmitTransitionLifecycleToJs()) {
1163+ this.eventEmitter!.emit("disappear", {})
1164+ }
1142 let arr = RNSScreen.TOP_PAGEID_ARR;1165 let arr = RNSScreen.TOP_PAGEID_ARR;
1143 const index = arr.findIndex(item => item === this.pageId);1166 const index = arr.findIndex(item => item === this.pageId);
1144 if (index !== -1) {1167 if (index !== -1) {
@@ -29,6 +29,7 @@ import { AnimateCallback, AnimationDefinition, CustomTransitionCoordinator } fro
29import { SafeAreaInsets } from '../utils/SafeAreaInsets';29import { SafeAreaInsets } from '../utils/SafeAreaInsets';
30import { RNSScreen } from './RNSScreen';30import { RNSScreen } from './RNSScreen';
31import { RNSScreenDescriptor } from './RNSScreen';31import { RNSScreenDescriptor } from './RNSScreen';
32+import { NavPathReorderLifecycleGate } from '../utils/NavPathReorderLifecycleGate';
32 33 
33export type RNSScreenStackDescriptor = Descriptor<"RNSScreenStack", RNC.RNSScreenStack.Props>34export type RNSScreenStackDescriptor = Descriptor<"RNSScreenStack", RNC.RNSScreenStack.Props>
34 35 
@@ -97,6 +98,55 @@ export struct RNSScreenStack {
97 this.isNativeBackOprFlag = flag;98 this.isNativeBackOprFlag = flag;
98 }99 }
99 100 
101+ /** stackUpadate 晚于 onFinish 时,转场结束后再留一小段抑制窗口 */
102+ private static readonly GATE_CLEAR_AFTER_TRANSITION_MS: number = 150;
103+ 
104+ private runNavPathReorderMutation(reason: string, mutate: () => void): void {
105+ NavPathReorderLifecycleGate.beginPathMutation(this.tag, this.stackController);
106+ mutate();
107+ NavPathReorderLifecycleGate.finishPathMutation(this.tag, this.stackController, reason);
108+ }
109+ 
110+ private clearNavReorderLifecycleGate(): void {
cpf-manager
cpf-managercpf-manager5月28日

【AI-Review】【一般】【基础代码问题】【资源使用问题】aboutToDisappear未清理NavPathReorderLifecycleGate静态状态

● 问题:clearNavReorderLifecycleGate() 方法已定义(第108行),但 aboutToDisappear() 中并未调用它。当 RNSScreenStack 组件销毁时,NavPathReorderLifecycleGate 中以 this.tag 为 key 的静态 Map 条目(transitionParticipantByStacksuppressTransitionLifecycleByStacklastReorderAffectedByStackpathSnapshotByStackclearTimerByStack)不会被清理。虽然 scheduleClear 定时器在大多数场景下最终会触发清理,但如果组件销毁时定时器已经触发完毕且无新操作,这些条目将永久驻留。

● 影响:一般。多个 RNSScreenStack 实例创建和销毁后,静态 Map 中累积的陈旧条目造成内存泄漏,长时间运行可能影响性能。

● 建议:在 aboutToDisappear() 中添加清理调用:

aboutToDisappear() {
  this.clearNavReorderLifecycleGate();
  // ... 其余清理逻辑
}
likedislike
yanPeng
yanPeng
5月28日 评论:
111+ NavPathReorderLifecycleGate.clear(this.tag);
112+ }
113+ 
114+ private scheduleClearNavReorderLifecycleGate(delayMs: number = 500): void {
115+ NavPathReorderLifecycleGate.scheduleClear(this.tag, delayMs);
116+ }
117+ 
118+ /** 在 finishTransitioning / screensNativeStackfinishTransitioning 之后清理 Gate */
119+ private scheduleClearNavReorderLifecycleGateAfterTransitionEnd(): void {
120+ this.scheduleClearNavReorderLifecycleGate(RNSScreenStack.GATE_CLEAR_AFTER_TRANSITION_MS);
121+ }
122+ 
123+ private emitFinishTransitioningToJs(actionType: string, operationId: string | null = null): void {
124+ this.eventEmitter!.emit("finishTransitioning", {});
cpf-manager
cpf-managercpf-manager5月31日

【AI-Review】【一般】【基础代码问题】【稳定性问题】emitFinishTransitioningToJs中eventEmitter非空断言存在崩溃风险

● 问题:emitFinishTransitioningToJs(第123行)中this.eventEmitter!.emit("finishTransitioning", {})使用了非空断言!。该方法被animateToonFinish回调(第996/1009行)调用,该回调通过闭包捕获了this。如果RNSScreenStack组件在动画执行期间被销毁(如页面快速切换、模块卸载),aboutToDisappear已被调用但eventEmitter未被显式置空,此时onFinish回调仍会触发并访问可能已失效的eventEmitter,导致崩溃。

类似地,this.ctx.rnInstance.emitDeviceEvent(...)中的this.ctx也存在同样风险。

● 影响:一般。在快速切换页面或模块卸载的时序下,组件销毁与动画完成回调存在竞态,可能导致应用崩溃。

● 建议:添加防御性检查:

private emitFinishTransitioningToJs(actionType: string, operationId: string | null = null): void {
  if (!this.eventEmitter) {
    return;
  }
  this.eventEmitter.emit("finishTransitioning", {});
  // ...
}

或在aboutToDisappear中取消正在执行的动画并清理回调引用。

likedislike
125+ this.scheduleClearNavReorderLifecycleGateAfterTransitionEnd();
126+ if (this.currentOperationType === 'REPLACE' && operationId) {
127+ this.ctx.rnInstance.emitDeviceEvent('screensNativeStackfinishTransitioning', {
128+ flag: true,
129+ tag: this.screenTag || this.tag,
130+ action: {
131+ type: actionType,
132+ _operationId: operationId
133+ }
134+ });
135+ } else {
136+ this.ctx.rnInstance.emitDeviceEvent('screensNativeStackfinishTransitioning', {
137+ flag: true,
138+ tag: this.screenTag || this.tag,
139+ action: {
140+ type: actionType
141+ }
142+ });
143+ }
144+ }
145+ 
146+ static shouldEmitScreenTransitionLifecycle(stackTag: number, screenTag: number): boolean {
cpf-manager
cpf-managercpf-manager5月28日

【AI-Review】【致命】【基础代码问题】【代码逻辑错误】shouldEmitScreenTransitionLifecycle 已定义但从未被调用,NavPathReorderLifecycleGate 抑制机制完全无效

● 问题:shouldEmitScreenTransitionLifecycle 静态方法已在 RNSScreenStack(第116行)中定义,它委托给 NavPathReorderLifecycleGate.shouldEmitTransitionLifecycleToJs() 来判断某个 Screen 的生命周期事件是否应发送到 JS 侧。然而,RNSScreen.ets 中的生命周期回调(onWillAppear、onAppear、onWillDisappear、onDisAppear,约第1097-1141行)均无条件地 emit 事件,从未调用此门控方法。RNSScreen.ets 第41行已 import RNSScreenStack,具备调用条件,但未接入。

这意味着 NavPathReorderLifecycleGate 正确地构建了 suppress 集合和 participants 集合,但这些数据从未被消费——所有 Screen 的 onWillAppear/onAppear/onWillDisappear/onDisAppear 事件仍会被无条件发送到 JS 侧,与未引入该 Gate 之前行为完全相同。整个 NavPathReorderLifecycleGate 模块的存在没有任何实际效果。

● 影响:致命。PR 的目标是修复 Screen 导航/重排时的生命周期事件问题,但核心门控逻辑未被接入,问题实际上没有被修复。当 NavPathStack 发生重排(如 stackUpadate、sortScreens)时,不应发送到 JS 侧的 onAppear/onDisAppear 事件仍然会被发送,可能导致 JS 侧的导航状态与原生侧不一致,进而引发页面闪烁、导航栈错乱等问题。

● 建议:在 RNSScreen.ets 的生命周期回调中加入门控检查。示例:

onWillAppear 回调:

.onWillAppear(() => {
  if (RNSScreenStack.shouldEmitScreenTransitionLifecycle(this.parentTag, this.tag)) {
    this.eventEmitter!.emit("willAppear", {})
  }
})

onAppear 回调:

.onAppear(() => {
  if (RNSScreenStack.shouldEmitScreenTransitionLifecycle(this.parentTag, this.tag)) {
    this.eventEmitter!.emit("appear", {})
  }
})

onWillDisappear 和 onDisAppear 回调同理。注意 onDisAppear 中除了 emit 事件外还有其他清理逻辑(如 releaseScreenOrientation、scheduleDeferredUnregister),这些清理逻辑不应被门控影响,仅门控 emit 调用即可。

likedislike
yanPeng
yanPeng
5月28日 评论:
147+ return NavPathReorderLifecycleGate.shouldEmitTransitionLifecycleToJs(stackTag, screenTag);
148+ }
149+ 
100 // ===== 修改:动画完成处理 =====150 // ===== 修改:动画完成处理 =====
101 private onAnimationComplete() {151 private onAnimationComplete() {
cpf-manager
cpf-managercpf-manager5月31日

【AI-Review】【严重】【基础代码问题】【代码逻辑错误】REPLACE操作完成时screensNativeStackfinishTransitioning事件被重复发送

● 问题:当REPLACE操作伴随原生返回键触发(backPressedFlag=true)时,screensNativeStackfinishTransitioning事件会被发送两次:

  1. animateToonFinish回调先调用this.backBtnPressed()(第993/1006行),backBtnPressed内部调用this.onAnimationComplete()
  2. onAnimationComplete()(第151行)发送一次screensNativeStackfinishTransitioning,包含operationIdduration字段,然后将this.currentReplaceOperationId置为null
  3. 随后onFinish继续调用this.emitFinishTransitioningToJs(actionType, opId)(第996/1009行),此时opId已被置为null,emitFinishTransitioningToJs再次发送screensNativeStackfinishTransitioning,但缺少_operationId

JS侧会收到两次screensNativeStackfinishTransitioning事件:第一次包含完整的操作ID,第二次缺失操作ID。第二次事件可能被JS侧误认为是另一个未跟踪的转场完成,导致导航状态混乱。

● 影响:严重。JS侧可能因为重复的完成事件导致导航栈状态不一致,具体表现为:_operationId匹配失败导致操作确认遗漏,或重复的状态更新引发竞态条件。

● 建议:将onAnimationComplete中的screensNativeStackfinishTransitioning发送逻辑合并到emitFinishTransitioningToJs中,消除重复发送。在emitFinishTransitioningToJs中补充operationIdduration字段:

private emitFinishTransitioningToJs(actionType: string, operationId: string | null = null): void {
  this.eventEmitter!.emit("finishTransitioning", {});
  this.scheduleClearNavReorderLifecycleGateAfterTransitionEnd();
  const payload: ESObject = {
    flag: true,
    tag: this.screenTag || this.tag,
    action: {
      type: actionType,
      ...(operationId ? { _operationId: operationId } : {})
    }
  };
  if (actionType === 'REPLACE' && operationId) {
    payload.operationId = operationId;
    payload.duration = Date.now() - this.replaceStartTime;
  }
  this.ctx.rnInstance.emitDeviceEvent('screensNativeStackfinishTransitioning', payload);
}

同时将onAnimationComplete简化为仅做操作记录和清理,不再单独发送事件。

likedislike
102 if (this.currentReplaceOperationId) {152 if (this.currentReplaceOperationId) {
@@ -113,6 +163,7 @@ export struct RNSScreenStack {
113 _operationId: this.currentReplaceOperationId163 _operationId: this.currentReplaceOperationId
114 }164 }
115 })165 })
166+ this.scheduleClearNavReorderLifecycleGateAfterTransitionEnd();
116 // 清理当前操作167 // 清理当前操作
117 this.currentReplaceOperationId = null168 this.currentReplaceOperationId = null
118 }169 }
@@ -163,12 +214,24 @@ export struct RNSScreenStack {
163 if (this.screenStackPath && this.screenStackPath.length >= 1) {214 if (this.screenStackPath && this.screenStackPath.length >= 1) {
164 this.screenStackPath.pop();215 this.screenStackPath.pop();
165 }216 }
217+ if (this.stack.length > 0 && newChild.length > 0) {
218+ NavPathReorderLifecycleGate.mergeTransitionParticipants(this.tag, [
219+ this.stack[this.stack.length - 1],
220+ newChild[newChild.length - 1]
221+ ]);
222+ }
166 }223 }
167 break;224 break;
168 case 'POP': {225 case 'POP': {
169 if (this.screenStackPath && this.screenStackPath.length >= 1) {226 if (this.screenStackPath && this.screenStackPath.length >= 1) {
170 this.screenStackPath.pop();227 this.screenStackPath.pop();
171 }228 }
229+ if (this.stack.length > 0 && newChild.length > 0) {
230+ NavPathReorderLifecycleGate.mergeTransitionParticipants(this.tag, [
231+ this.stack[this.stack.length - 1],
232+ newChild[newChild.length - 1]
233+ ]);
234+ }
172 }235 }
173 break;236 break;
174 case 'REPLACE': {237 case 'REPLACE': {
cpf-manager
cpf-managercpf-manager5月28日

【AI-Review】【严重】【基础代码问题】【代码逻辑错误】updateScreenStack 中 REPLACE/POP_TO_TOP/POP_TO/RESET 操作缺少 mergeTransitionParticipants 调用

● 问题:在 updateScreenStack 方法中,GO_BACK(第186-191行)和 POP(第198-203行)操作调用了 NavPathReorderLifecycleGate.mergeTransitionParticipants(),将当前栈顶 Screen 和新栈顶 Screen 注册为转场参与者。然而,REPLACE(第206-215行)、POP_TO_TOP(第216-220行)、POP_TO(第221-228行)、RESET(第229-257行)操作均未调用 mergeTransitionParticipants

关键时序问题:stackUpadate 方法(第303-318行)在执行路径变更前调用了 suppressNavPathExceptParticipants,此时如果 updateScreenStack 中未注册任何参与者,则 suppress 集合将包含 NavPathStack 中的所有 Screen。随后 customNavContentTransitionHandler 会注册 from/to 参与者,但对于使用 pushPathByName(name, null, false)(无动画)的 stackUpadate 场景,customNavContentTransitionHandler 可能不会被触发,导致参与者始终为空,所有 Screen 的生命周期事件被持续抑制直到 scheduleClear 定时器触发(1200ms)。

● 影响:严重。在前述 shouldEmitScreenTransitionLifecycle 接入后,REPLACE/POP_TO_TOP/POP_TO/RESET 操作可能导致:(1) 活跃转场 Screen 被错误抑制(如果 stackUpadate 先于 customNavContentTransitionHandler 执行);(2) 无动画场景下参与者永远不被注册,导致 1200ms 窗口内所有生命周期事件被错误抑制。

● 建议:在 updateScreenStack 的 REPLACE、POP_TO_TOP、POP_TO、RESET 分支中补充 mergeTransitionParticipants 调用,注册活跃转场参与者。示例:

REPLACE 分支:

case 'REPLACE': {
  if (this.screenStackPath && this.screenStackPath.length >= 1) {
    NavPathReorderLifecycleGate.mergeTransitionParticipants(this.tag, [
      this.screenStackPath[this.screenStackPath.length - 1],
      newChild[newChild.length - 1]
    ]);
    // ... 原有逻辑
  }
}

POP_TO_TOP、POP_TO、RESET 分支同理,应根据实际转场的 from/to Screen 注册参与者。

likedislike
yanPeng
yanPeng
5月28日 评论:
@@ -269,18 +332,21 @@ export struct RNSScreenStack {
269 @State isPreventKeyboardPopUp: boolean = false;332 @State isPreventKeyboardPopUp: boolean = false;
270 333 
271 stackUpadate(newChildren: number[]) {334 stackUpadate(newChildren: number[]) {
272- const indexs: number[] = []335+ NavPathReorderLifecycleGate.suppressNavPathExceptParticipants(this.tag, this.stackController);
cpf-manager
cpf-managercpf-manager5月28日

【AI-Review】【严重】【基础代码问题】【代码逻辑错误】stackUpadate 在参与者未注册时抑制所有 Screen 生命周期事件

● 问题:stackUpadate 方法在执行导航路径变更前调用 suppressNavPathExceptParticipants,该方法将所有不在 transitionParticipantByStack 中的路径加入 suppress 集合。但 transitionParticipantByStack 的参与者仅在 updateScreenStack 的 GO_BACK/POP 分支和 customNavContentTransitionHandler 中注册。以下场景会导致参与者未注册:

  1. updateScreenStack 未被调用(isCommandReceivedFlagisStackUpdateFlag 为 false 时跳过)
  2. 操作类型为 REPLACE/POP_TO_TOP/POP_TO/RESET 时,updateScreenStack 中未调用 mergeTransitionParticipants

当参与者集合为空时,所有路径均被加入 suppress 集合,导致所有 Screen 的 willAppear/appear/willDisappear/disappear 事件在 1200ms 内全部被抑制。这些事件在 suppress 清除后不会重新触发,JS 侧将永久丢失这些生命周期通知。

● 影响:严重。在 REPLACE/POP_TO_TOP/POP_TO/RESET 等场景下,目标 Screen 的 appear 事件被错误抑制,JS 侧 useEffect 等依赖生命周期事件的逻辑不会执行,可能导致页面状态不一致或功能异常。

● 建议:在 stackUpadate 中根据当前操作类型显式注册参与者,或在 updateScreenStack 的 REPLACE/POP_TO_TOP/POP_TO/RESET 分支中补充 mergeTransitionParticipants 调用。例如在 REPLACE 分支中:

case 'REPLACE': {
  if (this.screenStackPath && this.screenStackPath.length >= 1) {
    NavPathReorderLifecycleGate.mergeTransitionParticipants(this.tag, [
      this.screenStackPath[this.screenStackPath.length - 1],
      newChild[newChild.length - 1]
    ]);
    // ... 原有逻辑
  }
}

对于 POP_TO_TOP/POP_TO/RESET,应将目标栈顶 Screen 和当前栈顶 Screen 作为参与者注册。

likedislike
yanPeng
yanPeng
5月28日 评论:
273- let curPaht = this.stackController?.getAllPathName();336+ this.runNavPathReorderMutation('stackUpadate', () => {
274- for (let i = 0; i < curPaht.length - 1; i++) {337+ const indexs: number[] = []
275- indexs.push(i)338+ let curPaht = this.stackController?.getAllPathName();
276- }339+ for (let i = 0; i < curPaht.length - 1; i++) {
277- this.stackController.removeByIndexes(indexs);340+ indexs.push(i)
278- for (let i = 0; i < newChildren.length - 1; i++) {
279- if(this.stackController?.getAllPathName().indexOf(newChildren[i].toString()) === -1){
280- this.stackController.pushPathByName(newChildren[i].toString(), null, false)
281 }341 }
282- }342+ this.stackController.removeByIndexes(indexs);
283- this.stackController.moveIndexToTop(0, false)343+ for (let i = 0; i < newChildren.length - 1; i++) {
344+ if (this.stackController?.getAllPathName().indexOf(newChildren[i].toString()) === -1) {
345+ this.stackController.pushPathByName(newChildren[i].toString(), null, false)
346+ }
347+ }
348+ this.stackController.moveIndexToTop(0, false)
349+ });
284 }350 }
285 351 
286 isReplaceScenario(newChild: number[], myStack1: number[]): boolean {352 isReplaceScenario(newChild: number[], myStack1: number[]): boolean {
@@ -415,7 +481,8 @@ export struct RNSScreenStack {
415 481 
416 if (differentElementIndex === -1 && newChildren.length < this.stack.length &&482 if (differentElementIndex === -1 && newChildren.length < this.stack.length &&
417 JSON.stringify(newChildren) === JSON.stringify(this.stack.slice(0, newChildren.length))) {483 JSON.stringify(newChildren) === JSON.stringify(this.stack.slice(0, newChildren.length))) {
418- this.stackController.popToIndex(newChildren.length - 1);484+ const popIdx = newChildren.length - 1;
485+ this.stackController.popToIndex(popIdx);
419 return;486 return;
420 }487 }
421 488 
@@ -440,10 +507,12 @@ export struct RNSScreenStack {
440 this.stackUpadate(newChildren);507 this.stackUpadate(newChildren);
441 return;508 return;
442 }509 }
443- this.stackController.clear();510+ this.runNavPathReorderMutation('updateStack.clear+push', () => {
444- for (let i = differentElementIndex; i < newChildren.length; i++) {511+ this.stackController.clear();
445- this.stackController.pushPathByName(newChildren[i].toString(), null, false)512+ for (let i = differentElementIndex; i < newChildren.length; i++) {
446- }513+ this.stackController.pushPathByName(newChildren[i].toString(), null, false)
514+ }
515+ });
447 this.isReset = true;516 this.isReset = true;
448 return;517 return;
449 }518 }
@@ -455,8 +524,8 @@ export struct RNSScreenStack {
455 this.stackController.pushPathByName(newChildren[i].toString(), null)524 this.stackController.pushPathByName(newChildren[i].toString(), null)
456 }525 }
457 } else {526 } else {
458- this.stackController.popToIndex(differentElementIndex === -1 ? newChildren.length - 1 :527+ const popIdx = differentElementIndex === -1 ? newChildren.length - 1 : differentElementIndex - 1;
459- differentElementIndex - 1)528+ this.stackController.popToIndex(popIdx)
460 for (let i = differentElementIndex; i < newChildren.length; i++) {529 for (let i = differentElementIndex; i < newChildren.length; i++) {
461 this.stackController.pushPathByName(newChildren[i].toString(), null)530 this.stackController.pushPathByName(newChildren[i].toString(), null)
462 }531 }
@@ -665,21 +734,23 @@ export struct RNSScreenStack {
665 }734 }
666 735 
667 if (paths && paths.length > 0 && screenTag) {736 if (paths && paths.length > 0 && screenTag) {
668- // 从列表中获取screenTag需要插入的位置737+ this.runNavPathReorderMutation('sortScreens', () => {
669- let index = -1;738+ // 从列表中获取screenTag需要插入的位置
670- for (let i = paths.length - 1; i >= 0; i--) {739+ let index = -1;
671- if (Number(paths[i]) < Number(screenTag)) {740+ for (let i = paths.length - 1; i >= 0; i--) {
672- index = i;741+ if (Number(paths[i]) < Number(screenTag)) {
673- break;742+ index = i;
743+ break;
744+ }
674 }745 }
675- }
676 746 
677- // screenTag入栈747+ // screenTag入栈
678- this.stackController?.pushPathByName(screenTag, null);748+ this.stackController?.pushPathByName(screenTag, null);
679- // 栈排序,从上面插入位置将栈中按序移至栈顶,重新排序749+ // 栈排序,从上面插入位置将栈中按序移至栈顶,重新排序
680- for (let i = index + 1; i <= paths.length; i++) {750+ for (let i = index + 1; i < paths.length; i++) {
681- this.stackController?.moveToTop(paths[i], false);751+ this.stackController?.moveToTop(paths[i], false);
682- }752+ }
753+ });
683 }754 }
684 }755 }
685 756 
@@ -735,6 +806,7 @@ export struct RNSScreenStack {
735 } else {806 } else {
736 RNSScreenStack.screenStackPathMap.set(this.tag, this.screenStackPath)807 RNSScreenStack.screenStackPathMap.set(this.tag, this.screenStackPath)
737 }808 }
809+ this.clearNavReorderLifecycleGate()
738 }810 }
739 811 
740 @Builder812 @Builder
@@ -775,6 +847,14 @@ export struct RNSScreenStack {
775 return undefined;847 return undefined;
776 }848 }
777 849 
850+ NavPathReorderLifecycleGate.mergeTransitionParticipants(this.tag, [
851+ from.name,
852+ to.name
853+ ]);
854+ if (operation === NavigationOperation.POP) {
855+ NavPathReorderLifecycleGate.suppressNavPathExceptParticipants(this.tag, this.stackController);
856+ }
857+ 
778 this.customTransitionCoordinator.operation = operation;858 this.customTransitionCoordinator.operation = operation;
779 if (this.customTransitionCoordinator.interactive) {859 if (this.customTransitionCoordinator.interactive) {
780 let customAnimation: NavigationAnimatedTransition = {860 let customAnimation: NavigationAnimatedTransition = {
@@ -782,6 +862,7 @@ export struct RNSScreenStack {
782 this.customTransitionCoordinator.flushDeferredUnregisters();862 this.customTransitionCoordinator.flushDeferredUnregisters();
783 this.customTransitionCoordinator.proxy = undefined;863 this.customTransitionCoordinator.proxy = undefined;
784 this.customTransitionCoordinator.interactive = false;864 this.customTransitionCoordinator.interactive = false;
865+ this.scheduleClearNavReorderLifecycleGateAfterTransitionEnd();
785 },866 },
786 transition: (transitionProxy: NavigationTransitionProxy) => {867 transition: (transitionProxy: NavigationTransitionProxy) => {
787 this.customTransitionCoordinator.proxy = transitionProxy;868 this.customTransitionCoordinator.proxy = transitionProxy;
@@ -909,28 +990,10 @@ export struct RNSScreenStack {
909 this.logger.error('animation e: ' + e);990 this.logger.error('animation e: ' + e);
910 }991 }
911 this.customTransitionCoordinator.flushDeferredUnregisters();992 this.customTransitionCoordinator.flushDeferredUnregisters();
912- this.eventEmitter!.emit("finishTransitioning", {})
913 this.backBtnPressed();993 this.backBtnPressed();
914 const actionType: string = this.currentOperationType || 'UNKNOWN';994 const actionType: string = this.currentOperationType || 'UNKNOWN';
915 const opId: string | null = this.currentReplaceOperationId;995 const opId: string | null = this.currentReplaceOperationId;
916- if (this.currentOperationType === 'REPLACE' && opId) {996+ this.emitFinishTransitioningToJs(actionType, opId);
917- this.ctx.rnInstance.emitDeviceEvent('screensNativeStackfinishTransitioning', {
918- flag: true,
919- tag: this.screenTag || this.tag,
920- action: {
921- type: actionType,
922- _operationId: opId
923- }
924- });
925- } else {
926- this.ctx.rnInstance.emitDeviceEvent('screensNativeStackfinishTransitioning', {
927- flag: true,
928- tag: this.screenTag || this.tag,
929- action: {
930- type: actionType
931- }
932- });
933- }
934 }, 50)997 }, 50)
935 return;998 return;
936 }999 }
@@ -940,29 +1003,10 @@ export struct RNSScreenStack {
940 this.logger.error('animation e: ' + e);1003 this.logger.error('animation e: ' + e);
941 }1004 }
942 this.customTransitionCoordinator.flushDeferredUnregisters();1005 this.customTransitionCoordinator.flushDeferredUnregisters();
943- this.eventEmitter!.emit("finishTransitioning", {})
944 this.backBtnPressed();1006 this.backBtnPressed();
945- 
946 const actionType2: string = this.currentOperationType || 'UNKNOWN';1007 const actionType2: string = this.currentOperationType || 'UNKNOWN';
947 const opId2: string | null = this.currentReplaceOperationId;1008 const opId2: string | null = this.currentReplaceOperationId;
948- if (this.currentOperationType === 'REPLACE' && opId2) {1009+ this.emitFinishTransitioningToJs(actionType2, opId2);
949- this.ctx.rnInstance.emitDeviceEvent('screensNativeStackfinishTransitioning', {
950- flag: true,
951- tag: this.screenTag || this.tag,
952- action: {
953- type: actionType2,
954- _operationId: opId2
955- }
956- });
957- } else {
958- this.ctx.rnInstance.emitDeviceEvent('screensNativeStackfinishTransitioning', {
959- flag: true,
960- tag: this.screenTag || this.tag,
961- action: {
962- type: actionType2
963- }
964- });
965- }
966 }1010 }
967 }, () => {1011 }, () => {
968 this.curTime = new Date().getTime();1012 this.curTime = new Date().getTime();
@@ -0,0 +1,206 @@
1+/**
2+ * MIT License
3+ *
4+ * Copyright (C) 2025 Huawei Device Co., Ltd.
5+ *
6+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7+ * of this software and associated documentation files (the "Software"), to deal
8+ * in the Software without restriction, including without limitation the rights
9+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+ * copies of the Software, and to permit persons to whom the Software is
11+ * furnished to do so, subject to the following conditions:
12+ *
13+ * The above copyright notice and this permission notice shall be included in all
14+ * copies or substantial portions of the Software.
15+ *
16+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+ * SOFTWARE.
23+ */
24+ 
25+export class NavPathReorderLifecycleGate {
26+ private static transitionParticipantByStack: Map<number, Set<string>> = new Map();
27+ private static suppressTransitionLifecycleByStack: Map<number, Set<string>> = new Map();
28+ private static lastReorderAffectedByStack: Map<number, Set<string>> = new Map();
29+ private static pathSnapshotByStack: Map<number, string[]> = new Map();
30+ private static clearTimerByStack: Map<number, number> = new Map();
31+ 
32+ static cancelScheduledClear(stackTag: number): void {
33+ const timer = NavPathReorderLifecycleGate.clearTimerByStack.get(stackTag);
34+ if (timer !== undefined) {
35+ clearTimeout(timer);
36+ NavPathReorderLifecycleGate.clearTimerByStack.delete(stackTag);
37+ }
38+ }
39+ 
40+ static beginPathMutation(stackTag: number, pathStack: NavPathStack | undefined): void {
41+ NavPathReorderLifecycleGate.cancelScheduledClear(stackTag);
42+ const paths = pathStack?.getAllPathName() ?? [];
43+ NavPathReorderLifecycleGate.pathSnapshotByStack.set(stackTag, [...paths]);
44+ }
45+ 
46+ static finishPathMutation(stackTag: number, pathStack: NavPathStack | undefined, reason: string): void {
47+ const before = NavPathReorderLifecycleGate.pathSnapshotByStack.get(stackTag) ?? [];
48+ NavPathReorderLifecycleGate.pathSnapshotByStack.delete(stackTag);
49+ const after = pathStack?.getAllPathName() ?? [];
50+ const affected = NavPathReorderLifecycleGate.computeReorderAffected(before, after);
51+ const participants =
52+ NavPathReorderLifecycleGate.transitionParticipantByStack.get(stackTag) ?? new Set<string>();
53+ const suppress = NavPathReorderLifecycleGate.suppressTransitionLifecycleByStack.get(stackTag) ??
54+ new Set<string>();
55+ 
56+ if (reason === 'stackUpadate') {
57+ NavPathReorderLifecycleGate.addNonParticipantsToSuppress(before, after, participants, suppress);
58+ } else if (affected.size > 0) {
59+ const mergedAffected = NavPathReorderLifecycleGate.lastReorderAffectedByStack.get(stackTag) ?? new Set();
60+ affected.forEach((tag: string) => mergedAffected.add(tag));
61+ NavPathReorderLifecycleGate.lastReorderAffectedByStack.set(stackTag, mergedAffected);
62+ affected.forEach((tag: string) => {
63+ if (!participants.has(tag)) {
64+ suppress.add(tag);
65+ }
66+ });
67+ }
68+ 
69+ if (suppress.size > 0) {
70+ NavPathReorderLifecycleGate.suppressTransitionLifecycleByStack.set(stackTag, suppress);
71+ }
72+ }
73+ 
74+ static suppressNavPathExceptParticipants(
75+ stackTag: number,
76+ pathStack: NavPathStack | undefined
77+ ): void {
78+ NavPathReorderLifecycleGate.cancelScheduledClear(stackTag);
79+ const participants =
80+ NavPathReorderLifecycleGate.transitionParticipantByStack.get(stackTag) ?? new Set<string>();
81+ const suppress = NavPathReorderLifecycleGate.suppressTransitionLifecycleByStack.get(stackTag) ??
82+ new Set<string>();
83+ const paths = pathStack?.getAllPathName() ?? [];
84+ paths.forEach((tag: string) => {
85+ if (!participants.has(tag)) {
86+ suppress.add(tag);
87+ }
88+ });
89+ if (suppress.size > 0) {
90+ NavPathReorderLifecycleGate.suppressTransitionLifecycleByStack.set(stackTag, suppress);
91+ }
92+ }
93+ 
94+ private static addNonParticipantsToSuppress(
95+ before: string[],
96+ after: string[],
97+ participants: Set<string>,
98+ suppress: Set<string>
99+ ): void {
100+ const all = new Set<string>();
101+ before.forEach((t: string) => all.add(t));
102+ after.forEach((t: string) => all.add(t));
103+ all.forEach((tag: string) => {
104+ if (!participants.has(tag)) {
105+ suppress.add(tag);
106+ }
107+ });
108+ }
109+ 
110+ /** 路径顺序变化、入栈/出栈的 tag 均视为被重排影响 */
111+ private static computeReorderAffected(before: string[], after: string[]): Set<string> {
112+ const affected = new Set<string>();
113+ const allTags = new Set<string>();
114+ before.forEach((t: string) => allTags.add(t));
115+ after.forEach((t: string) => allTags.add(t));
116+ 
117+ allTags.forEach((tag: string) => {
118+ const iBefore = before.indexOf(tag);
119+ const iAfter = after.indexOf(tag);
120+ if (iBefore === -1 || iAfter === -1) {
121+ affected.add(tag);
122+ return;
123+ }
124+ // stackUpadate 后 B 等中间页可能仍在 path 内,但 index 变化会触发 ArkUI 生命周期
125+ if (iBefore !== iAfter) {
126+ affected.add(tag);
127+ }
128+ });
129+ 
130+ const commonBefore: string[] = [];
131+ const commonAfter: string[] = [];
132+ before.forEach((t: string) => {
133+ if (after.indexOf(t) !== -1) {
134+ commonBefore.push(t);
135+ }
136+ });
137+ after.forEach((t: string) => {
138+ if (before.indexOf(t) !== -1) {
139+ commonAfter.push(t);
140+ }
141+ });
142+ if (JSON.stringify(commonBefore) !== JSON.stringify(commonAfter)) {
cpf-manager
cpf-managercpf-manager5月28日

【AI-Review】【建议】【基础代码问题】【性能和效率问题】computeReorderAffected 使用 JSON.stringify 比较数组顺序

● 问题:computeReorderAffected 方法在第 132 行使用 JSON.stringify(commonBefore) !== JSON.stringify(commonAfter) 来判断两个数组的元素顺序是否一致。这种方式存在两个问题:

  1. 性能:JSON.stringify 需要遍历整个数组并生成字符串,时间复杂度 O(n),加上字符串比较 O(n),总复杂度较高。此方法已在 allTags.forEach 循环中使用了 indexOf(O(n)),使得整体复杂度为 O(n²)。
  2. 鲁棒性:JSON.stringify 的结果依赖于元素的字面量表示,如果 tag 值包含特殊字符(虽然当前为数字字符串,不构成实际问题),可能导致比较结果不符合预期。

● 影响:建议。当前 NavPathStack 路径数量通常较小(< 20),性能影响有限。但使用直接循环比较更清晰、更高效。

● 建议:替换为直接循环比较:

let orderChanged = false;
if (commonBefore.length !== commonAfter.length) {
  orderChanged = true;
} else {
  for (let i = 0; i < commonBefore.length; i++) {
    if (commonBefore[i] !== commonAfter[i]) {
      orderChanged = true;
      break;
    }
  }
}
if (orderChanged) {
  commonBefore.forEach((t: string) => affected.add(t));
  commonAfter.forEach((t: string) => affected.add(t));
}
likedislike
143+ commonBefore.forEach((t: string) => affected.add(t));
144+ commonAfter.forEach((t: string) => affected.add(t));
145+ }
146+ return affected;
147+ }
148+ 
149+ static mergeTransitionParticipants(stackTag: number, tags: Array<string | number | undefined>): void {
150+ let set = NavPathReorderLifecycleGate.transitionParticipantByStack.get(stackTag);
151+ if (!set) {
152+ set = new Set<string>();
153+ NavPathReorderLifecycleGate.transitionParticipantByStack.set(stackTag, set);
154+ }
155+ tags.forEach((t) => {
156+ if (t !== undefined && t !== null) {
157+ set!.add(t.toString());
158+ }
159+ });
160+ }
161+ 
162+ static shouldEmitTransitionLifecycleToJs(stackTag: number, screenTag: number): boolean {
163+ const key = screenTag.toString();
164+ const suppress = NavPathReorderLifecycleGate.suppressTransitionLifecycleByStack.get(stackTag);
165+ if (!suppress || suppress.size === 0) {
166+ return true;
167+ }
168+ if (!suppress.has(key)) {
169+ return true;
170+ }
171+ const participants = NavPathReorderLifecycleGate.transitionParticipantByStack.get(stackTag);
172+ return participants !== undefined && participants.has(key);
173+ }
174+ 
175+ static getSuppressTags(stackTag: number): string[] {
176+ const suppress = NavPathReorderLifecycleGate.suppressTransitionLifecycleByStack.get(stackTag);
177+ if (!suppress) {
178+ return [];
179+ }
180+ return Array.from(suppress);
181+ }
182+ 
183+ static clear(stackTag: number): void {
184+ const timer = NavPathReorderLifecycleGate.clearTimerByStack.get(stackTag);
185+ if (timer !== undefined) {
186+ clearTimeout(timer);
187+ NavPathReorderLifecycleGate.clearTimerByStack.delete(stackTag);
188+ }
189+ NavPathReorderLifecycleGate.transitionParticipantByStack.delete(stackTag);
190+ NavPathReorderLifecycleGate.suppressTransitionLifecycleByStack.delete(stackTag);
191+ NavPathReorderLifecycleGate.lastReorderAffectedByStack.delete(stackTag);
192+ NavPathReorderLifecycleGate.pathSnapshotByStack.delete(stackTag);
193+ }
194+ 
195+ /** stackUpadate 常在 NavTransition.onFinish 之后触发,需延迟清理 suppress */
196+ static scheduleClear(stackTag: number, delayMs: number = 500): void {
197+ const old = NavPathReorderLifecycleGate.clearTimerByStack.get(stackTag);
198+ if (old !== undefined) {
199+ clearTimeout(old);
200+ }
201+ const timer = setTimeout(() => {
202+ NavPathReorderLifecycleGate.clear(stackTag);
203+ }, delayMs) as number;
204+ NavPathReorderLifecycleGate.clearTimerByStack.set(stackTag, timer);
205+ }
206+}