Angular 里 OnPush 子组件接轮询数据不刷新,先换数组引用还是先补 markForCheck?
Angular 里 OnPush 子组件接轮询新数据不刷新,多数不是变更检测坏了,而是这次变化没落在 OnPush 认的通道里。OnPush 的语义是把「每轮都查」收紧成「有明确信号才查」:模板事件、@Input 引用变化、async pipe 或 signal 新值、手动 markForCheck 才会唤起这一轮。按 2026 年交付经验,先确认外部回调是否收敛、数组引用是否替换,再决定要不要补标记;多数情况只需换成新引用,不必退回 Default。
一、OnPush 改的是触发时机,不是把检测关掉
Angular 默认从根组件往下走一遍变更检测,组件多、模板表达式重时开销会放大。OnPush 给组件加了一道准入门槛:只有 Angular 认为这个组件可能有变化,才检查它和子树。它是把「每轮都查」收紧成「有信号才查」,不是跳过刷新。
- 模板里的事件绑定(点击、输入、提交等)在这个组件内触发;
- @Input 传入值的引用发生变化,是换对象或数组,不是只改属性;
- 模板里 async pipe 订阅的 Observable 发出新值;
- 使用 signal 时,模板读取的 signal 值发生变化;
- 代码里显式调用 markForCheck(),或 detectChanges() 立即检查当前视图及子树。
出现「不刷新」时,逐条对照这次改动落在哪一类;落在外面,就必须补一条 Angular 能感知的通道。
二、数据不刷新时,按四问核对
这四个问题按顺序问,是为了避免一上来就加 markForCheck。手动标记散在业务代码里,后面很难判断哪次刷新是该刷新,哪次是被强行刷新,排查成本会转移给接手的人。
- 触发源在哪:这次变化是模板事件触发,还是订阅回调、定时器、第三方 SDK 回调带来?后者 Angular 不一定感知,常见做法是收敛到统一入口再标记。
- 引用变了吗:数组是 push、对象是改属性,还是整体替换?OnPush 比较引用,值变了和引用变了是两回事。
- 标记给到谁:markForCheck 把沿途祖先标脏、等下一轮;detectChanges 立即检查当前视图及子视图,它不会回头重算父级模板表达式,用错会漏刷新或增开销。
- 这个组件真需要 OnPush 吗:如果内部状态频繁自变、又不在性能瓶颈上,用 Default 反而省心,别把性能策略当代码规范硬推。
把这四问走完,再决定是换引用、补标记,还是保持 Default,通常比直接加标记更稳。
三、交付现场:轮询列表的约束、做法与代价
更常见的现场是:后台列表页两个人、两周排期,数据来自三个接口加一条轮询,还要嵌在 WebView 里。做法是把行组件改 OnPush,列表输入统一用新数组替换并加 trackBy,外部轮询回调只在入口调一次 markForCheck,并把「谁负责标记」写进验收清单。结果是滚动时检测次数下降、交互更稳;但第一版漏标了推送分支,线上出现「有新数据不刷新」的偶发问题,返工排查加回归大约半天到一天。这类代价的常见区间是半天到一天,取决于回归覆盖是否包含外部回调和错误态;根因多数不是 OnPush 难,而是标记责任没写清。
四、Default、OnPush 与 signal 输入的常见区间对比
三种做法不是新旧替代关系,而是适用面不同。下面按触发方式、性能表现、上手与调试成本、改造周期、适用对象拆开看,周期与幅度均为经验区间,实际取决于页面规模和团队熟悉度。
- 触发方式:Default 每轮都查;OnPush 靠模板事件、新引用、async pipe、signal 或手动标记;signal 输入把值变化本身变成模板可追踪依赖。
- 性能表现:大列表、纯展示组件上 OnPush 检测次数下降更明显;简单表单页收益有限,常见区间只是小幅波动。
- 上手与调试成本:OnPush 要求团队理解引用与标记,熟悉期常见每人一到三天;signal 写法直观些,但旧代码迁移要分批,混用期更考验约定。
- 改造周期:中小页面单组件改造含回归常见半天到两天;列表加轮询类页面常见两到五天。
- 适用对象:展示型、列表型、输入驱动型组件适合 OnPush;内部可变状态多、需求一天一变的原型页,先保持 Default 更划算。
对比的目的不是选一个更先进的写法,而是确认这次不刷新该归到哪条通道。
五、适用与不适用边界
OnPush 不是越早加越好的开关。它适合性能瓶颈已经出现、组件输入输出边界清楚的场景;对还在快速改需求的早期页面,先把组件边界和数据流理顺,比先加策略更划算。
- 适合:纯展示的行组件、卡片组件;大列表和频繁输入的父组件拆出来的子组件;已经定位到某块模板表达式过重、检测开销大的页面。
- 不适合:组件内部靠订阅、定时器频繁改自身状态的页面;团队没人愿意维护标记规则的短期项目;只有一两个页面、尚无性能压力的展示站。
判断标准可以落成一句:组件数据如果主要来自输入和模板事件,就适合 OnPush;如果大量刷新来自内部命令式赋值、订阅和定时器,先把这些通道收敛,再谈策略。这一步不做,直接加 OnPush,问题往往只是从「卡」变成「偶尔不刷新」。
常见问题
OnPush 组件里 this.list.push() 后页面不刷新,加 markForCheck 就够了吗?
不一定。标记通常能救回当前这一次,但输入仍是被改动的同一引用,下次父组件再传同一数组还会漏刷新,常见做法是改成新数组再赋值。
async pipe 在 OnPush 组件里能自动触发检查吗?
能。async pipe 收到新值时会标记该视图需要检查,所以把订阅放模板里,通常比组件内手动订阅再赋值省事,也少一处退订负担。
父组件也是 OnPush,子组件换了新对象仍不更新,先查什么?
先查父组件里这个新对象是不是产生于 Angular 能感知的通道,例如手动订阅、定时器或第三方回调;父级没被标记,子级就等不到这一轮检查。
把组件从 Default 改成 OnPush,回归时间一般留多少?
按经验区间,中小页面留半天到一天,列表加轮询类页面留一到两天,重点回归外部回调、路由复用后返回和错误态,不只点主流程。
真要动手改,建议先挑一个纯展示的行组件试点:输入改成不可变更新、加上 trackBy、把外部回调收敛到一个标记入口,跑一轮回归再决定铺开范围。如果页面数据大量来自组件内部命令式赋值、团队又没人维护标记规则,先保持 Default 并把组件拆小,通常比强行上 OnPush 更稳。
-
Flutter 嵌的 H5 活动页,安卓上滑动发涩,到底该先动哪一层?
日期:2026年10月14日 阅读:60
-
2026年了,页面 title 在源码里看不到,百度收录的标题是从哪来的?
日期:2026年10月13日 阅读:51
-
2026年了,Vue 里两个弹窗共用同一个组合式函数,一个提交失败,另一个也弹出错误提示,是状态串了吗?
日期:2026年10月11日 阅读:107
-
React 里只改了一个主题色,为什么侧边栏和列表都跟着刷新?
日期:2026年10月10日 阅读:63
-
2026年了,批量补数据用 forEach 里写 await,为什么请求全发出去才等结果?
日期:2026年10月9日 阅读:122




