因为专注所以专业
助力成长与创新,汇集前沿前端开发观点

Angular 里 OnPush 子组件接轮询数据不刷新,先换数组引用还是先补 markForCheck?

2026年10月12日 阅读:69

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。手动标记散在业务代码里,后面很难判断哪次刷新是该刷新,哪次是被强行刷新,排查成本会转移给接手的人。

  1. 触发源在哪:这次变化是模板事件触发,还是订阅回调、定时器、第三方 SDK 回调带来?后者 Angular 不一定感知,常见做法是收敛到统一入口再标记。
  2. 引用变了吗:数组是 push、对象是改属性,还是整体替换?OnPush 比较引用,值变了和引用变了是两回事。
  3. 标记给到谁:markForCheck 把沿途祖先标脏、等下一轮;detectChanges 立即检查当前视图及子视图,它不会回头重算父级模板表达式,用错会漏刷新或增开销。
  4. 这个组件真需要 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 更稳。

有类似的项目需求?
联系我们,获取一对一项目参考方案
获取方案
对这个话题感兴趣?
10 年技术团队,24 小时内出具参考方案
获取方案
准备好开始了吗,
那就与我们取得联系吧!
13370032918
了解更多服务,随时联系我们
请填写您的需求
您希望我们为您提供什么服务呢
您的预算
联
系
微信二维码
扫码添加客服微信
专业对接各类技术问题
联系电话
13370032918 (金经理)
电话若占线或未接到、就加下微信
联系邮箱
349077570@qq.com
提交成功
感谢您的信任,我们会尽快与您联系!
为您推荐以下案例