| @@ -207,6 +207,21 @@ export struct RNSScreen { | |||
| 207 | return this.currentOffset | 207 | 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、transparentModal | 334 | // presentation 值为modal、transparentModal |
| 320 | // cardStyleInterpolator 值为forModalPresentationIOS | 335 | // 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 () => { |
| 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 中离场页的 AnimateCallback | 1160 | // 延后 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 | |||
| 29 | import { SafeAreaInsets } from '../utils/SafeAreaInsets'; | 29 | import { SafeAreaInsets } from '../utils/SafeAreaInsets'; |
| 30 | import { RNSScreen } from './RNSScreen'; | 30 | import { RNSScreen } from './RNSScreen'; |
| 31 | import { RNSScreenDescriptor } from './RNSScreen'; | 31 | import { RNSScreenDescriptor } from './RNSScreen'; |
| 32 | +import { NavPathReorderLifecycleGate } from '../utils/NavPathReorderLifecycleGate'; | ||
| 32 | 33 | ||
| 33 | export type RNSScreenStackDescriptor = Descriptor<"RNSScreenStack", RNC.RNSScreenStack.Props> | 34 | export 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 { | ||
【AI-Review】【一般】【基础代码问题】【资源使用问题】aboutToDisappear未清理NavPathReorderLifecycleGate静态状态 ● 问题: ● 影响:一般。多个 ● 建议:在
![]() ![]() | |||
| 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", {}); | ||
【AI-Review】【一般】【基础代码问题】【稳定性问题】emitFinishTransitioningToJs中eventEmitter非空断言存在崩溃风险 ● 问题: 类似地, ● 影响:一般。在快速切换页面或模块卸载的时序下,组件销毁与动画完成回调存在竞态,可能导致应用崩溃。 ● 建议:添加防御性检查:
或在 ![]() ![]() | |||
| 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 { | ||
【AI-Review】【致命】【基础代码问题】【代码逻辑错误】shouldEmitScreenTransitionLifecycle 已定义但从未被调用,NavPathReorderLifecycleGate 抑制机制完全无效 ● 问题: 这意味着 ● 影响:致命。PR 的目标是修复 Screen 导航/重排时的生命周期事件问题,但核心门控逻辑未被接入,问题实际上没有被修复。当 NavPathStack 发生重排(如 stackUpadate、sortScreens)时,不应发送到 JS 侧的 onAppear/onDisAppear 事件仍然会被发送,可能导致 JS 侧的导航状态与原生侧不一致,进而引发页面闪烁、导航栈错乱等问题。 ● 建议:在 onWillAppear 回调:
onAppear 回调:
onWillDisappear 和 onDisAppear 回调同理。注意 onDisAppear 中除了 emit 事件外还有其他清理逻辑(如 releaseScreenOrientation、scheduleDeferredUnregister),这些清理逻辑不应被门控影响,仅门控 emit 调用即可。 ![]() ![]() | |||
| 147 | + return NavPathReorderLifecycleGate.shouldEmitTransitionLifecycleToJs(stackTag, screenTag); | ||
| 148 | + } | ||
| 149 | + | ||
| 100 | // ===== 修改:动画完成处理 ===== | 150 | // ===== 修改:动画完成处理 ===== |
| 101 | private onAnimationComplete() { | 151 | private onAnimationComplete() { |
【AI-Review】【严重】【基础代码问题】【代码逻辑错误】REPLACE操作完成时screensNativeStackfinishTransitioning事件被重复发送 ● 问题:当REPLACE操作伴随原生返回键触发(
JS侧会收到两次 ● 影响:严重。JS侧可能因为重复的完成事件导致导航栈状态不一致,具体表现为: ● 建议:将
同时将 ![]() ![]() | |||
| 102 | if (this.currentReplaceOperationId) { | 152 | if (this.currentReplaceOperationId) { |
| @@ -113,6 +163,7 @@ export struct RNSScreenStack { | |||
| 113 | _operationId: this.currentReplaceOperationId | 163 | _operationId: this.currentReplaceOperationId |
| 114 | } | 164 | } |
| 115 | }) | 165 | }) |
| 166 | + this.scheduleClearNavReorderLifecycleGateAfterTransitionEnd(); | ||
| 116 | // 清理当前操作 | 167 | // 清理当前操作 |
| 117 | this.currentReplaceOperationId = null | 168 | 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': { |
【AI-Review】【严重】【基础代码问题】【代码逻辑错误】updateScreenStack 中 REPLACE/POP_TO_TOP/POP_TO/RESET 操作缺少 mergeTransitionParticipants 调用 ● 问题:在 关键时序问题: ● 影响:严重。在前述 shouldEmitScreenTransitionLifecycle 接入后,REPLACE/POP_TO_TOP/POP_TO/RESET 操作可能导致:(1) 活跃转场 Screen 被错误抑制(如果 stackUpadate 先于 customNavContentTransitionHandler 执行);(2) 无动画场景下参与者永远不被注册,导致 1200ms 窗口内所有生命周期事件被错误抑制。 ● 建议:在 REPLACE 分支:
POP_TO_TOP、POP_TO、RESET 分支同理,应根据实际转场的 from/to Screen 注册参与者。 ![]() ![]() | |||
| @@ -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); |
【AI-Review】【严重】【基础代码问题】【代码逻辑错误】stackUpadate 在参与者未注册时抑制所有 Screen 生命周期事件 ● 问题:
当参与者集合为空时,所有路径均被加入 suppress 集合,导致所有 Screen 的 willAppear/appear/willDisappear/disappear 事件在 1200ms 内全部被抑制。这些事件在 suppress 清除后不会重新触发,JS 侧将永久丢失这些生命周期通知。 ● 影响:严重。在 REPLACE/POP_TO_TOP/POP_TO/RESET 等场景下,目标 Screen 的 appear 事件被错误抑制,JS 侧 useEffect 等依赖生命周期事件的逻辑不会执行,可能导致页面状态不一致或功能异常。 ● 建议:在
对于 POP_TO_TOP/POP_TO/RESET,应将目标栈顶 Screen 和当前栈顶 Screen 作为参与者注册。 ![]() ![]() | |||
| 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 | @Builder | 812 | @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(); |


【AI-Review】【严重】【基础代码问题】【稳定性问题】onShown回调未受Gate保护,NavPathStack重排时可能污染全局导航状态
● 问题:PR对
onWillAppear、onAppear、onWillDisappear、onDisAppear四个生命周期回调都加了shouldEmitTransitionLifecycleToJs()守卫,但onShown回调(第1122行)未做同样的守卫检查。onShown中更新了RNSScreen.TOP_PAGEID、RNSScreen.TOP_PAGEID_ARR、RNSScreen.TOP_PAGE_PRESENTATION_MAP、RNSScreen.TOP_PAGE_PREVENT_MAP、RNSScreen.TOP_PAGE_TAG_MAP等全局静态状态。当NavPathStack发生重排(如
stackUpadate、sortScreens)时,ArkUI Navigation框架可能触发中间屏幕的onShown回调。此时非栈顶屏幕的onShown会将TOP_PAGEID设置为错误的pageId,导致:onBackPressed回退到错误的屏幕isFirstScreenInStack判断错误TOP_PAGE_PRESENTATION_MAP、TOP_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回调中补充对当前真实栈顶屏幕的状态刷新逻辑。