ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Angular核心原理面试指南:变更检测、依赖注入与RxJS实战

Angular核心原理面试指南:变更检测、依赖注入与RxJS实战 Angular面试这四个字敲进搜索栏要么是你收到了面试邀约开始临阵磨枪要么是看到周围同事跳槽后终于下决心系统刷一遍框架底层。我见过太多人把Angular面试题大全背了一周进面之后被一个追问直接卡死——懒加载模块里注入的服务为什么可能不是单例OnPush策略下为什么数组push不触发更新这种问题没法背因为它考的不是记忆而是你是否真的理解框架帮你做了什么。这篇文章是Angular持续提升系列第6篇围绕面试高频的核心原理展开变更检测、依赖注入、RxJS异步处理、生命周期与组件通信、路由与性能优化。每一块我都会先讲清楚机制再拆解几个真实的高频面试题最后给出可以照着说的回答思路。适合准备Angular中高级前端岗面试的同学也适合工作几年想系统梳理一次框架底层的朋友。1. 面试官在Angular面试中真正考察的三条主线1.1 为什么背八股文越来越不好使现在的Angular面试尤其是中高级岗位已经很少考说说路由有哪几种守卫这种纯记忆题了。原因很简单框架知识点就那么多背一个月谁都背得下来面试官根本无法从中判断你的真实水平。他们更倾向于抛一个真实场景——搜索框实时查询怎么做列表数据量大滑动卡顿怎么排查看你在现场能不能把知识串成解决方案。我辅导过不少准备跳槽的前端有个普遍问题能背出生命周期8个钩子的顺序但被问到为什么ngOnInit适合做初始化而constructor不适合时会卡住知道OnPush能优化性能但被追问为什么数组push不生效就懵了。这说明他们记的是表格不是原理。而这一篇就是帮大家把这些考点从记住变成理解。背八股之所以是背是因为缺失了为什么这一层。当你把每个知识点的动机——它解决了什么问题、不解决会怎样——想明白了面试题怎么换花样你都能接住。这正好也是面试官区分背题型选手和真懂型选手的核心手段。1.2 主线一变更检测与数据流Angular作为框架最大的价值之一就是数据变了视图自动跟着变。这个能力来源于它的变更检测系统。面试官在这个方向上的追问逻辑非常固定先问你知不知道数据绑定的底层机制再给你一个反直觉场景比如OnPush下数组变更不更新最后让你给出定位和优化方案。理解变更检测本质上要理解三件事检测由谁触发事件、定时器、请求等异步任务、检测从哪开始从根组件向下遍历、检测怎么判断变没变比较绑定表达式的值分Default和OnPush两种策略。后面第2章会把这条线完整串起来包括Zone.js怎么感知异步任务以及新版本里Signal是怎样改变这一切的。1.3 主线二依赖注入的作用域思维依赖注入DI是Angular区别于其他前端框架的标志性设计。面试官考DI重心通常不在什么是控制反转这种概念题上而在作用域和实例数量上同一个Service在不同地方注入拿到的到底是不是同一个对象为什么懒加载模块里会出现多个单例这个考点需要建立注入器层级的心智模型。Angular里的注入器不是只有根级一个而是跟着模块、组件形成了一棵有层级的树。理解了这棵树很多所谓的玄学bug其实都有明确答案。后面第3章会把这个模型完整画出来并给出几个真实工程里的应用案例。1.4 主线三异步与响应式编程Angular整个体系都是建立在RxJS之上的HttpClient返回Observable表单的valueChanges是Observable路由事件是Observable甚至连Output事件流都长着一张可观察对象的脸。所以面试官特别爱出场景题怎么实现搜索防抖、怎么防止重复提交、怎么避免内存泄漏。这类题目表面考操作符实际考的是对流的理解流怎么产生、怎么合并、怎么取消、怎么在组件销毁时清理。你把RxJS的核心思维理清楚这类题基本就是送分。第4章我会从几个高频场景题入手把常用操作符的选择逻辑讲透。2. 变更检测与Zone.jsAngular核心原理的必考题2.1 从一次点击事件看变更检测的全过程先说结论Angular默认的变更检测是全局遍历式的。你在页面上点击一个按钮触发click事件事件处理函数改了一个组件的某个属性。事件一旦执行完毕Angular的NgZone会感知到这次异步任务结束然后调用ApplicationRef.tick()从根组件开始自上而下遍历整棵组件树对每个组件执行变更检测把绑定表达式的旧值和新值作比较有差异就更新DOM。这个过程听起来开销很大但Angular做了优化组件树是沿着Injector和View节点走的遍历本身不是最重的开销真正贵的是每次都要对表达式求值。所以Angular团队才一直在推进更细粒度的更新方案Signal后面会单独说。这里有个重要细节变更检测的触发点不只是click任何异步任务——setTimeout、setInterval、Promise resolve、HTTP回调、EventEmitter——只要跑在Zone内结束后都会引发一次全局检测。我见过有人问为什么我什么都没动页面自己就刷新了原因通常就是某个定时器或者后台请求触发了检测把其他组件里的脏数据一起带着更新了。结合我的经验理解这个全树遍历的模型是回答变更检测类面试题的前提。因为你后面说的OnPush、markForCheck、runOutsideAngular本质上都是在跟这个默认策略做博弈——要么缩小检测范围要么减少不必要的触发。2.2 Default与OnPush两种策略的分水岭默认策略Default下父组件每次检查子树中的每个组件都会跟着检查一遍。这样简单、可靠但数据量一大就有性能压力。Angular给出的一个标准优化手段就是ChangeDetectionStrategy.OnPush。OnPush策略的核心逻辑是一个OnPush组件只会在四种情况下被检查组件的Input属性引用发生变化也就是父组件传进来了一个新对象/新数组组件自身绑定了事件这个事件被触发后组件模板里用了async pipe上游Observable发射了值手动调用了ChangeDetectorRef.markForCheck()或detectChanges()。特别注意第一点它强调的是引用变化而不是内容变化。所以面试官最爱问的那个问题就来了一个OnPush组件接收了父组件的数组你在父组件里arr.push(4)界面会更新吗不会。因为push不改变数组引用OnPush认为输入没变直接跳过检测自然不更新。解决办法是重新赋值一个新数组[...arr]或者手动markForCheck。我实际做过一个小验证在父组件里用setInterval隔一秒push一条数据到数组子组件用OnPush。结果子组件视图纹丝不动但如果在push之后再用展开运算符赋一次新数组视图立刻刷新。这个例子建议你亲手跑一遍比背结论印象深得多。2.3 Zone.js的原理与面试经典追问Zone.js是Angular变更检测的触发器。它的核心思路是把浏览器的宏任务、微任务、事件监听、Promise、定时器等异步API全部打补丁patch让它们在执行前后可以被框架感知。Angular启动时在NgZone自己的Zone上下文里运行应用异步任务结束时会emit一个microTaskEmpty事件Angular收到后就去执行一轮变更检测。面试里常见的追问有两个。第一个是Zone.js patch了哪些API答setTimeout、setInterval、Promise、addEventListener、XMLHttpRequest/fetch等。凡是能产生异步回调的基本都被覆盖了。第二个是Zone.js的性能代价是什么答每次异步任务结束都要做全局检测patch本身也让一些API调用链变长。这正好引出了Signal和zone-less的未来方向。另外如果你写的代码跑在Zone之外——比如用了zone.js没覆盖的某些原生API、或者调用runOutsideAngular()——异步完成后不会自动触发变更检测。这个特性也是优化手段高频但不需要更新视图的定时器比如轮询上报位置可以放到runOutsideAngular里跑减少不必要的全树检测。我工作中就踩过一次坑三方的WebSocket库自己封装了底层连接回调没走zone导致消息推过来后界面不刷新。排查了半天最后发现是zone的边界问题。从此我学到一个经验凡是异步确实执行了但视图没更新的诡异问题除了检查OnPush之外还要想一想这个异步到底跑没跑在Angular的Zone里。2.4 实战问题还原列表页卡顿怎么定位和优化面试官页面有个上千行的列表输入关键字过滤时会卡顿你从哪些角度排查这个问题把变更检测、数据处理、渲染策略全考了。我的回答思路分三步。第一步先怀疑变更检测的规模。是不是每次输入事件都把整棵组件树检测了一遍如果列表组件是Default策略建议改成OnPush给列表项加上trackBy让Diff阶段可以复用已有DOM节点而不是把整个列表销毁重建。第二步看过滤计算本身。如果每次输入都在内存里遍历几千条数据重新生成数组而且这个计算量在事件回调里同步执行输入自然会卡。给输入加debounceTime让过滤逻辑在100~300ms的静默期之后再执行。第三步如果列表真的太大考虑分页或者虚拟滚动cdk-virtual-scroll-viewport只渲染可视区域内的行。再配合Angular DevTools的profiler实际测量一下变更检测耗时到底集中在哪里而不是盲猜。这题答到用Profiler定位再针对性优化这个层面面试官基本就满意了因为这是真实项目里的做法不是背出来的。2.5 Signal信号新一代响应式面试新宠Angular 16引入的Signal是面试题里越来越热的东西。它解决的核心痛点是默认的Zone.js全局检测一刀切没法精确知道哪个组件依赖哪个状态。信号则把响应式粒度做到了状态级别——你用signal()创建状态在组件里调用它读取框架就能记录这个组件对该状态的依赖当signal的值变化时只有真正依赖它的组件才被安排更新不需要整树遍历。面试如果被问到Signal和OnPush的关系可以这样答OnPush本质上是一种手动标记的优化它回答的是哪些组件应该检查Signal则提供了一种更自然的机制——谁读取了状态谁就自动订阅更新连标记都省了。Angular 17/18里大量团队正在做的signal-based组件迁移其实是Angular走向zone-less的关键一步。从面试准备角度至少要能说出signal()创建状态、computed()派生计算值、effect()注册副作用在组件里读取信号模板中自动更新对于高频更新的场景Signal比OnPush手动markForCheck体验好很多。这也是Angular持续提升系列后面几篇会重点展开的话题。3. 依赖注入从控制反转概念到provide/inject实战3.1 用生活类比讲清DI到底解决了什么问题依赖注入DI这个词太抽象但它的意图其实很朴素对象A需要用到服务BA不需要自己new一个B而是通过构造函数声明我有个B的依赖由注入器在创建A时把B塞进去。类比一下就是你下馆子吃饭不需要自己买菜、开火、洗碗只要对服务员说我要一份宫保鸡丁后厨自然会按需做好端上来。后厨就是注入器菜谱就是服务注册表。好处是哪天厨师换了做法B的实现换了你不需要改自己那份点单逻辑后厨内部处理就行。在Angular里服务用Injectable管起来构造参数自动被解析测试时想mock某个依赖只需要注入器里换一个provider被测组件代码一行不用改。这就是依赖注入工程上的最大价值可测试、可替换、生命周期可控。面试时被问DI有什么好处从这三个维度答比空讲解耦要扎实得多。3.2 层级注入器为什么服务是单例这句话不完整面试里被问Angular的服务是单例的吗最严谨的回答是取决于你在哪个注入器里拿它。Angular的注入器不是只有一台全局后厨而是层级化的根注入器RootInjector应用启动时创建providedIn:root的服务都放在这层全局唯一。环境注入器EnvironmentInjector每个懒加载模块会创建自己的模块级注入器。元素注入器ElementInjector跟组件实例绑定组件providers数组里声明的服务放在这里。注入器查询依赖时默认从当前组件往上找先找组件自己的ElementInjector然后向上找父组件的ElementInjector再上一层是模块级环境注入器最后是RootInjector。每一层找不到再往上直到NullInjector才报错。这个模型可以解释一个经典bug某Service只在AppModule的providers里注册理论上全局可见但如果你在一个懒加载模块的providers里也注册了同名Service那么该懒加载模块里的组件拿到的是新实例而其他组件拿到的是根实例。两边各改各的状态表面上都是单例实际却是两套。所以团队规范里通常建议跨模块共享的服务一律用providedIn:root不要在懒加载模块里重复providers。我自己在项目里就遇到过一次登录后在用户服务里存了用户信息跳到某个懒加载业务模块后发现里面读出来的是空的。查了半天发现那个模块的providers里也注入了一份UserService。这个坑在真实项目里太常见了面试官拿它来考你对注入器层级的理解一抓一个准。3.3 providedIn、InjectionToken与三种Provider写法高频考点回答框架先搞清楚一个容易混淆的点Injectable({providedIn: root})与在AppModule的providers数组里注册效果在大多数场景下是一样的——服务都挂在RootInjector。区别在于providedIn:root能做到Tree-shaking如果这个服务最终没被注入到任何地方它会被从构建产物里摇掉而模块providers里的写法做不到这一点。再说InjectionToken。当你需要注入的不是类而是配置对象、常量时不能用类作token否则会和别的类混淆。这时用InjectionToken定义一个唯一的token再provide它一个值或工厂函数组件里用Inject(TOKEN)取出来。比如一个全局配置对象CONFIG_TOKEN就可以在不同环境用useValue提供不同配置。Provider的三种常用写法也要能说清useClass提供一个新的类创建时按类的实例化方式创建。useExisting给已有token起个别名指向同一个实例适合做抽象接口与实现的绑定。useFactory通过工厂函数创建实例可以在函数里读配置、判断环境、注入其他依赖后再定制化返回。useValue最常用于InjectionToken的常量配置。面试题里如果问怎么在不同环境开发/生产注入不同的服务实现先答useClass或useFactory再答用环境配置作为工厂入参。这样回答既有层次又体现了你对DI机制的理解不是停留在概念层面。3.4 实战如何统一替换第三方日志服务说个真实场景项目里很多组件直接注入了一个第三方日志SDK代码里到处是logService.info(...)。后来公司要求日志要同时上报到自己的监控平台你不能把每个组件都改一遍。最优雅的解法是利用DI的可替换性定义一个自己的抽象token比如LogService可以是抽象类或InjectionToken然后写一个CustomLogService实现它在providers里用{ provide: LogService, useExisting: CustomLogService }或useClass覆盖。所有组件继续注入LogService改一行providers全项目生效。这就是依赖注入解耦在真实工程里最有价值的地方。如果团队里碰到某个Service要按用户权限返回不同配置的需求也可以用工厂Providerprovide那个Service时useFactory注入AuthService和配置文件在工厂函数里根据权限返回对应配置项服务本身保持无状态。工厂Provider这种创建逻辑集中管理的思路在面试里讲出来非常加分。4. RxJS与异步处理面试场景题的高频出题区4.1 Angular为什么选了RxJS而不是Promise面试官如果问Angular为什么用Observable你最好别说因为Angular用了RxJS那等于没说。正确的角度是Angular的海量异步场景天然需要流而不是一次性结果。Promise的特点是只能产生一个值、不能取消、没有时间概念。但前端真实场景里一次搜索请求被更早的输入覆盖、一个表单值连续变化、一个进度条连续上报进度这些都是多个异步值随时间到来的典型流式场景。Observable恰好支持多值、支持取消订阅、有一整套操作符做组合变换。还有个细节值得提HttpClient虽然是一次请求返回一个值但它返回的也是Observable而不是Promise。因为Observable是惰性的——只有subscribe时才真正发请求这样就能天然实现用switchMap取消过期请求这类行为。如果是Promise产生了请求之后就没法反悔了。很多面试者会把注意力放在哪个更强大上其实真正该说的是Angular的所有异步API都是为流式处理设计的Observable是唯一自洽的选择。4.2 搜索防抖一道高频场景题的完整实现搜索框实时搜索既要减少请求又要防竞态怎么写这是Angular面试里出现频率极高的题。标准解法searchInput.valueChanges .pipe( debounceTime(300), distinctUntilChanged(), switchMap(keyword this.searchService.search(keyword)), takeUntil(this.destroy$) ) .subscribe(results this.results results);逐个说明debounceTime(300)用户停止输入300ms后才放行避免每敲一个键都发请求。distinctUntilChanged()过滤掉重复关键字比如用户输入angular又删回angula再补上angular值没变就不触发。switchMap()一旦有新的输入值过来自动取消上一个还在pending的请求保证页面上展示的是最后一次输入的结果——这一步解决竞态问题。takeUntil(destroy$)在组件销毁时退订避免内存泄漏。如果更完整一些还要在catchError里做错误处理并返回一个空流避免一次请求失败把整个搜索流杀死。这块是区分背过答案和真写过的关键因为实际项目里接口一定会时不时挂一下没有错误处理的生产代码是活不过一周的。4.3 内存泄漏与takeUntil一个常见坑的两种死法Angular里最容易踩的RxJS坑就是订阅后忘了退订。举个例子你在constructor里直接subscribe了一个service的流组件销毁后订阅者还在回调还会继续触发如果回调里又更新了DOM就会报错甚至造成内存泄漏。规范的解法是在ngOnDestroy里执行destroy$.next() destroy$.complete()所有订阅统一takeUntil。但这里有坑takeUntil的位置如果写错会导致意外行为。正确做法是把它放在整条管道里所有其他操作符之后、subscribe之前这样源流可以正常传递数据只有收到destroy信号才终止。如果误放到了中间比如takeUntil放在switchMap前面那destroy的时候switchMap可能来不及处理上一次的请求行为会有差异。第二个常见死法destroy$是一个Subject结果在ngOnDestroy里只调用了next()没有complete()。这会导致所有订阅虽然收到了终止信号但Subject自己还活着如果还有上游继续向它推数据仍会往下传导。所以标准动作是nextcomplete一起调。如果是模板里的订阅更推荐直接AsyncPipe它会在组件销毁时自动退订代码还更简洁。我自己现在写Angular凡是模板要渲染的流一律用async或signal手动subscribe的场景大幅减少内存泄漏问题从源头递减。面试聊到这个习惯通常会让面试官觉得你维护过真实的长线项目。4.4 switchMap/exhaustMap/mergeMap/concatMap四大操作符的选择逻辑面试官很喜欢问防重复提交用什么。很多人背答案是exhaustMap却说不清为什么。其实这四个操作符的选择逻辑一句话就能概括取决于你希望旧请求和新请求如何相处。switchMap新值来了取消旧任务执行新任务。适合搜索自动补全、标签切换。exhaustMap新值来了如果当前任务还在执行直接忽略新值。适合登录按钮、提交表单——用户狂点按钮只有一个请求发出。mergeMap新旧任务并发执行互不干扰。适合同时拉取用户资料和权限列表这类没有依赖关系的并发请求。concatMap新任务排队等上一个任务完成再执行。适合逐个上传文件这种要求有序的场景。面试时如果只答防重复提交用exhaustMap这只是第一层。能再说出如果登录请求成功了才允许再次点击用exhaustMap还是更稳一点的并发控制之类的话分量就完全不同。我自己项目里喜欢exhaustMap再加一个按钮loading态双保险因为只有exhaustMap还不足以阻止用户在请求返回后立刻再次点击。4.5 HTTP拦截器里的Observable冷热流分析进阶加分项HttpClient的拦截器是面试里容易出彩的加分点。它的原理是每个HTTP请求发出时会顺着拦截器链依次经过每个interceptor每个interceptor拿到的是请求对象返回的是一个新的Observable供下游订阅。这里有个进阶问题同一个HTTP请求对象如果被多个人订阅会发生什么默认是每次订阅都重新发请求因为HttpClient返回的是冷Observable。但如果你在某个拦截器里想做缓存——比如对相同URL的GET请求只发一次——就需要用shareReplay(1)把一次请求的结果广播给所有订阅者。我在一个实际项目里做过一个简单的GET缓存拦截器map到响应后用shareReplay缓存命中缓存就跳过真实请求刷新时又手动清缓存。这题如果能讲明白冷/热和shareReplay的引用计数面试官会对你另眼相看。因为它已经超出了背API的范畴进入设计模式的层面了。5. 生命周期与组件通信基础题里挖出的深坑5.1 生命周期顺序与为什么ngOnInit里能读输入、constructor不能生命周期钩子是Angular面试的基础题但基础不等于简单。完整顺序是constructor → ngOnChanges → ngOnInit → ngDoCheck → ngAfterContentInit → ngAfterContentChecked → ngAfterViewInit → ngAfterViewChecked → ngOnDestroy。注意几个关键点ngOnChanges只在Input绑定的值发生引用变化时触发首次进入时它会在ngOnInit之前先触发一次。ngOnInit在首次ngOnChanges完成后执行此时Input已经有值适合做依赖输入属性的初始化逻辑。ngDoCheck在每次变更检测轮次中都会执行哪怕是OnPush组件被跳过检测它也可能被调用它本身是每次检测都跑的钩子所以不要在里做重逻辑。ngAfterViewInit是首次视图渲染完成后触发ViewChild查询结果此时才稳定可用。面试里最典型的追问就是为什么不能在constructor里读取Input答案很简单constructor执行时组件实例刚创建输入属性还没由框架赋值当然拿不到。同时constructor里也不适合做重逻辑因为组件可能还没挂载服务也可能还没完全可用。这个为什么想通了你就能理解生命周期钩子的设计意图每个钩子都对应组件生命中的一个特定阶段工具就该用在正确的阶段。5.2 组件通信方式全景什么时候用哪种组件通信是Angular实际开发里每天都要面对的问题面试官也爱从你会怎么选型切入。我整理一张清单Input/Output父子组件最直接的方式适合父传子数据、子抛事件给父。模板引用变量 ViewChild父组件主动获取子组件实例适合父要调用子组件方法的场景。ContentChild / 内容投影ng-content父组件往子组件插槽里塞模板适合可复用容器组件如Tab、Dialog。服务 Subject/Observable跨层、跨模块、兄弟组件之间共享状态或广播事件最通用但也最容易滥用。依赖注入把某个父组件实例provide给更深层子组件用适合树状结构里的隐式通信。路由参数 / ActivatedRoute页面级传参。NgRx / Signal Store大型应用全局状态管理状态复杂且需要可预测的更新时用。选型的判断标准是通信半径父子之间能用Input/Output就不要引入全局service全局service能搞定的事就不要直接上NgRx。过度设计在Angular项目里比不过度设计更容易把自己绕晕。面试时能把为什么这个场景不用那个方案讲清楚比单纯列清单有价值得多。5.3 动态组件与ViewChild的渲染时机问题动态创建组件也是面试常客通常和弹窗、动态表单绑定出现。Angular 13之后动态创建组件的主流写法是用ViewContainerRefconst componentRef viewContainerRef.createComponent(SomeComponent);createComponent会立即创建组件并把它插入到视图容器中。这里要小心的是如果SomeComponent依赖了某些服务需要在创建前手动provide如果SomeComponent自己声明了providers那这些依赖会自动用它的ElementInjector解决不需要额外处理。另一个高频坑是把createComponent放在ngOnInit里然后用ViewChild来拿刚创建的组件实例——拿到的可能是null。原因是ViewChild查询结果要在视图初始化完成ngAfterViewInit之后才稳定。动态组件插入后变更检测会把握渲染时机但你的读引用动作要跟对生命周期钩子。现实项目里我一般会把动态组件的实例存到一个数组中管理而不是依赖ViewChild去轮询。5.4 markForCheck和detectChanges到底怎么选这两个API是OnPush相关的核心考点。很多人的认知是界面不更新就调一下detectChanges其实分不清会让代码很丑。markForCheck()作用是标记当前组件从自身到根节点路径上的所有OnPush组件为需要检查。它不立即执行检测而是等下一轮变更检测周期统一处理。使用场景异步回调里更新了OnPush组件的数据但这次更新不在Angular能自动感知的范围内比如setTimeout里改了字段。detectChanges()立即从当前组件开始向下执行一次变更检测同步更新DOM。这个要慎用因为它会绕过正常的检测调度如果调用次数多了或者用在根组件附近代价很大而且容易破坏数据驱动视图的一致性。一个典型场景某OnPush组件里setTimeout(1000)改了一个字段界面没动。原因是OnPush的触发条件里setTimeout不是组件自身事件不会自动标记。正确修法是回调里调用this.cdRef.markForCheck()。如果项目已经在用Signal这个问题会消解一大半——signal更新时会自动通知依赖组件去更新不再需要手动markForCheck这也是Signal在面试里强势起来的原因之一。6. 路由与性能优化给面试答案增加工程深度6.1 懒加载与预加载不只是拆包两个字路由懒加载是所有Angular性能优化话题的起点。配置方式是{ path: user, loadChildren: () import(./user/user.module).then(m m.UserModule) }这块面试高频题是懒加载之后首屏加载快了很多但用户点击进入子模块时还要等网络下载有没有办法优化答案是用预加载策略。Angular内置了PreloadAllModules但你可以在路由data里自定义预加载优先级。一个工程化很常见的做法是实现自定义PreloadingStrategy根据navigator.connection.effectiveType判断当前网络是4g/3g还是慢速只在网络条件好时预加载项目里也可以对用户大概率会访问的模块设置priority1优先预加载其余等空闲再加载。这样既保住了首屏体积又减少了次级页面切换的等待感。面试时能提到自定义预加载策略说明你真正部署过大型应用而不是只写过Demo。6.2 路由守卫五种守卫的本质区别与应用场景路由守卫的面试题基本围绕能不能用、什么时候用来展开这里用一张表理清守卫作用时机典型场景CanActivate进入路由前登录校验、权限检查CanActivateChild进入子路由前对某个模块做统一鉴权CanDeactivate离开当前路由前表单未保存时弹确认框Resolve路由激活前预取数据进入详情页前先拉数据CanMatch/CanLoad模块是否加载按用户权限决定是否加载整个代码块回答时值得加一句注意事项守卫里返回Observable或Promise时要注意它必须最终发出一个值否则路由会一直卡在Loading状态。Angular 15之后推荐用函数式守卫配合inject()代码更短且能避免类守卫在测试时的this上下文问题。这个细节也能体现你跟上框架演进节奏了。6.3 性能优化清单面试简答的加分配置面试官问Angular性能优化你做了哪些最怕的是只甩名词。我建议从两个维度展开第一维度是减少不必要的变更检测组件设OnPush数据采用不可变更新列表用trackBy指定稳定key避免整个列表重建高频但不影响视图的逻辑用runOutsideAngular减少异步任务引发的全树检测局部不需要实时同步的子树用ChangeDetectorRef.detach()摘掉需要渲染时reattach()再接回来。第二维度是减少主线程负载大数据列表用虚拟滚动只渲染可视区重型计算放worker或者预计算模板表达式禁副作用路由懒加载自定义预加载控制网络负载。最后可以提一句用Angular DevTools的profiler去看变更检测耗时分布而不是靠感觉。面试官听到这里基本就会觉得你有实际调试经验。6.4 现场题页面初始化慢从框架角度能做哪些事这类开放性题要把前面所有知识串起来。我的回答框架先定位——用Angular DevTools和Network面板看是首包太大、接口慢还是渲染慢。然后分层优化——首包太大就懒加载按需引入移除死代码接口慢就考虑缓存、CDN、优化接口聚合渲染慢就查变更检测规模列表量大的上虚拟滚动有OnPush空间的加OnPush。这样回答的优点是它不是某一条死知识而是一套定位→分层→验证的思维面试官追问哪个层次你都能接住。而且这种回答方式在任何性能相关的问题里都可以复用本质上它训练的是排错方法论而不是某道题的答案。被问Angular核心原理是什么时我习惯用三句话法则组织先讲框架帮你解决了什么数据变更自动同步视图、依赖自动注入、异步流式处理再挑一个最熟的比如变更检测展开细节最后提一下边界OnPush下哪些不触发、Signal如何改变未来方向。这样既照顾了广度又展现了深度。还有一个小建议是准备一个自己实际调过的性能问题作为故事面试官对真实场景的兴致永远比纯知识点高。我自己面试别人时最想听的也不是标准答案而是候选人怎么描述他当时排查问题的路径——从哪里怀疑、怎么验证、最后怎么确认。这些东西我自己的体会是面试官越往后问看的越是你有没有把知识变成判断力。这套Angular持续提升系列后续的文章也会继续沿着这个思路把更多高频考点的底层逻辑拆开讲。祝准备面试的朋友都能拿到心仪的Offer。
返回列表