想象你走进一家新开的智慧大食堂。别的食堂里,每位厨师炒菜前都得亲自跑去仓库搬食材、翻冰柜找酱料、再到调料架摸索半天;这家食堂却有一条看不见的后勤流水线:你只要对着配菜台喊一声「青椒肉丝套餐」,配菜总管就把切好的肉丝、酱料、葱姜蒜按标准份量递到你手边。你的小冰柜里没有的食材,它还会自动向总仓库调货。 更妙的是,后厨的订单从来不是一张张静止的纸条,而是一根永远在流水的水管:新订单像水滴一样源源不断流进来,沿途经过滤网去掉垃圾订单、经过调味器自动加料、经过限流阀防止客人的疯狂点单把后厨冲垮,最后才变成一盘盘热气腾腾的菜端出窗口。 这套智慧后厨,就是 Angular 这座庞大框架的两大引擎:一套负责「自动配菜、就地取材」,让代码不再到处手动拼装;另一套负责「让数据像水一样流动、加工」,把混乱的异步事件整理得井井有条。读完这篇文章,你会从零看懂这两套引擎,并知道它们如何在一家真实的 AI 应用餐厅里配合运转。

岗位关联

对 AI 从业者来说,Angular 的依赖注入与响应式编程是大型前端应用的「骨架与血脉」:学会它,你就能亲手搭建 AI 对话、模型看板这类复杂交互系统,从画页面进阶到设计系统。

目录
  1. 可视化演示
  2. DI核心原理:自动配菜台
  3. 装饰器体系:菜单上的印章
  4. 注入器层次:多级仓库
  5. RxJS基础:水管与水流
  6. 操作符管线:净水器流水线
  7. 实战应用:把智慧食堂开起来
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Angular依赖注入层级(Injector Tree)与RxJS操作符管道(map/filter/reduce),动态演示依赖注入流和Observable数据流弹珠图。

本节配有交互动效(进入视口后自动加载,约 3KB)。

依赖注入:Injector层级解析 青色为Root Injector(根注入器) 浅青为Module/Component层级 绿色为Service(服务实例) DI: Component -> Injector -> Token -> Instance Angular DI通过Injector层级树解析依赖,组件构造函数声明Token,Injector按层级查找Provider创建实例

DI核心原理:自动配菜台

回到智慧大食堂。假设你是新来的炒菜师傅,要做一道「鱼香肉丝」。传统小饭馆里,你得自己跑去菜窖挖猪肉、翻冰柜找豆瓣酱、再到调料架摸索糖和醋——一道菜还没炒,先累掉半条命。而智慧食堂的规矩完全不同:你只需要在工位上拿起话筒喊一声「我要鱼香肉丝的料」,配菜总管就会把肉丝、酱料、葱姜蒜按标准份量递到你手边。你只管炒,不用管这些食材从哪儿来、怎么搬运。

这个「喊单就有菜」的过程,就是软件世界里的控制反转(IoC):把「谁负责找依赖」的权力,从组件自己手里反转给一个中央容器。Angular 的依赖注入(Dependency Injection,简称 DI)就是这套配菜机制的工程实现。它带来最直接的改变是:代码里不再到处 new 服务对象,组件与依赖之间的耦合被解开了。

在 Angular 中,组件在构造函数中声明它需要的服务,例如 constructor(private api: ApiService),注入器就会根据类型 token(相当于食材编号)找到对应的 Provider(提供者),创建或取出实例并注入进来。Provider 可以配置成多种形态:useClass 用某个类创建、useValue 直接给一个现成对象、useFactory 调用工厂函数动态生成。这样做的结果是:组件不再「自己 new 依赖」,代码耦合度大降,测试时可以轻松换成 Mock 版本。

这就是智慧食堂后厨的第一条纪律:厨师不进货,只喊单。你写的组件不应该 new 自己的服务,而应该在构造函数里「点菜」,让配菜总管去解决。一眼看去,代码里充满了构造函数参数,可每一个参数背后,都有一套自动配菜的后勤系统在运转。

deps(C)={token1,token2,…,tokenn},instance(C)=Injector(deps(C))\text{deps}(C) = \{ \text{token}_1, \text{token}_2, \dots, \text{token}_n \}, \qquad\text{instance}(C) = \text{Injector}\bigl(\text{deps}(C)\bigr)
左边是组件声明的「依赖清单」,右边是注入器按清单配好实例——厨师只需喊单,不必知道食材从哪来。
  1. 依赖清单\text{deps}(C) 表示组件 C 在构造函数里「喊单」的依赖集合,每个 token 对应一种食材编号
    deps(C)={token1,token2,…,tokenn}\text{deps}(C) = \{ \text{token}_1, \text{token}_2, \dots, \text{token}_n \}
  2. 注入器装配\text{Injector}(\cdot) 是配菜总管,它逐项读取清单,找到对应 Provider 并创建或取出实例
    instance(C)=Injector(deps(C))\text{instance}(C) = \text{Injector}\bigl(\text{deps}(C)\bigr)
  3. 装配结果\text{instance}(C) 得到一个依赖已被填好的组件,可以直接使用,无需关心食材从哪来
    instance(C)=inject(deps(C))\text{instance}(C) = \text{inject}(\text{deps}(C))

装饰器体系:菜单上的印章

食堂里每道菜谱上都盖着印章:「半成品可复用」「需冷藏」「三号窗口专供」。这些印章不改变菜的做法,却决定了后勤部门怎么对待它:有的要放冰柜,有的要热锅保温。Angular 里的装饰器,就是给类盖的这种印章。

装饰器(Decorator)本质上是一个函数,它接收类,返回一个带着元数据的类。Angular 靠这些元数据决定:这个类是不是服务(@Injectable)、是不是组件(@Component)、是不是模块(@NgModule)。你把「印章」盖在类上面,框架的「后勤总管」就知道该怎么安排它——是入库管理,还是端上桌招待客人。

具体来说,@Injectable 标记一个类可以被注入器创建和注入,通常还配上 providedIn: 'root' 指定注册到根注入器;@Component 除了标记组件,还携带 selector、template、styles 等视图配置;@NgModule 则把组件、指令、服务、导入模块打包成一个「餐牌分区」。这些装饰器让 Angular 在编译期和运行期都能读取配置,进而自动完成实例化与装配。

菜单上的印章决定了后勤如何备菜。写 Angular 时,看到 @Injectable 就知道这是「可被配菜总管管理的食材」,看到 @Component 就知道这是一道「可以端上桌的菜」,看到 @NgModule 就知道这是「一块完整的分区菜单」。有了印章,整个后厨的自动化才有了依据。

decorate(class)=(class,  {metadata:providedIn,  selector,  template})\text{decorate}(\text{class}) = \bigl(\text{class},\; \{\text{metadata}: \text{providedIn},\; \text{selector},\; \text{template}\}\bigr)
装饰器给类盖上一个「元数据便签」,Angular 运行时读到便签就知道这个类该怎么被对待。
  1. 原类入参\text{class} 是被盖章的原始类,本身不携带任何框架信息
    class↦decorate(class)\text{class} \mapsto \text{decorate}(\text{class})
  2. 元数据附加\text{decorate}(\cdot) 给类绑定配置项,把 providedIn、selector、template 等写进元数据
    metadata={providedIn,selector,template}\text{metadata} = \{ \text{providedIn}, \text{selector}, \text{template} \}
  3. 结构化输出返回的二元组让框架在运行时能读取配置,从而决定实例化与装配策略
    decorate(class)=(class,metadata)\text{decorate}(\text{class}) = \bigl(\text{class}, \text{metadata}\bigr)

注入器层次:多级仓库

智慧食堂不可能只有一个大仓库:总仓库在楼下,每个档口还有自己的小冰柜,放该档口常用的食材。炒菜师傅要姜末,先看自己档口的冰柜,没有才打电话向总仓库调货。Angular 的注入器也是这样的树状层级。

所谓层级注入器(Hierarchical Injectors),指的是 Angular 会按模块和组件的嵌套关系,形成一棵注入器树。组件在自己的注入器上找不到依赖时,会沿着父级一路向上查找,直到根注入器。这既保证了共享服务的单例性,也让某个子树能临时「覆写」依赖,提供特供版本——就像档口冰柜里可以放自己调制的秘制酱料。

具体来说,providedIn: 'root' 的服务注册在根注入器,全应用共享;而组件上的 providers: [...] 会在该组件及其子树内创建新的注入器,相当于档口冰柜。同一个 token 在子注入器被覆写后,子树内的组件拿到的是子版本,子树外不受影响。合理利用这一机制,可以实现「全局单例 + 局部特供」的灵活架构。

档口冰柜的哲学是「就近取材,向上求助」。排查依赖问题时,不妨顺着注入器树一层层问:这个食材是我自己冰柜有的,还是从总仓库调来的?理解了层级,你就不会再为「为什么两个组件拿到的不是同一个实例」而困惑。

resolve(c,t)={provider(c,t),if ∃ p∈providers(c)resolve(parent(c), t),otherwise\text{resolve}(c, t) =\begin{cases}\text{provider}(c, t), & \text{if } \exists\, p \in \text{providers}(c) \\[4pt]\text{resolve}\bigl(\text{parent}(c),\, t\bigr), & \text{otherwise}\end{cases}
查找依赖像翻冰柜:自己档口有就立即用,没有就一层层向上级仓库问,直到总仓库。
  1. 本级货架\text{provider}(c, t) 表示在组件 c 自己的注入器中查找 token t,相当于先翻自己档口的冰柜
    provider(c,t)∈providers(c)\text{provider}(c, t) \in \text{providers}(c)
  2. 条件分支\text{if } \exists\, p 表示本级注入器存在该 token 的 Provider 时直接命中并返回
    ∃ p∈providers(c)\exists\, p \in \text{providers}(c)
  3. 向上递归\text{parent}(c) 表示沿父级注入器继续查找,直到根注入器,找不到才抛错
    resolve(parent(c), t)\text{resolve}\bigl(\text{parent}(c),\, t\bigr)

RxJS基础:水管与水流

厨房里的订单不是一本静止的账本,而是一条随时有新水滴进来的水管。前台每来一位客人,就有一滴订单水珠掉进管道;后厨想接水,就得打开水龙头——这就是 RxJS 的直觉模型。

RxJS 的核心是 Observable(可观察对象):它本身是一张「接水方案」,创建时不做任何事,只有调用 subscribe 才真正开始发射数据,并返回一个 Subscription(订阅凭证),用来在不需要时取消订阅(unsubscribe)。观察者对象上还有 next、error、complete 三个回调,分别对应来水、漏水、停水三种事件。Angular 的异步操作,比如 HTTP 请求、路由参数变化、表单输入,全都建立在这样一根根水管之上。

Subject 是一种特殊的 Observable,既是数据源又是订阅者,适合做多播广播——多个消费者共享同一条水流。BehaviorSubject 则更进一步,它必须提供初始值,并会把最近一次的值自动重放给新订阅者,就像公告栏永远贴着最新公告,谁来看都能一眼看到。在 Angular 中,配合 async pipe 使用这些流,模板会自动订阅与销毁,省去手动管理订阅的麻烦。

水管的比喻帮你记住三个关键时刻:创建时不流水(惰性)、订阅才开闸、取消订阅才停水。写 Angular 异步代码时,凡是「一会儿可能来数据」的地方,都可以想象成一根水管:要么自己 subscribe 并记得关闭,要么干脆交给 async pipe 这个「自动水龙头」。

Observable:observer↦subscription,next(x):observer→data\text{Observable} : \text{observer} \mapsto \text{subscription}, \qquad\text{next}(x) : \text{observer} \to \text{data}
Observable 是「接水方案」,subscribe 是开水闸,next 是每次滴进来的水珠——三者关系就是 RxJS 的骨架。
  1. 惰性函数\text{Observable} 创建时并不流水,只是保存了一张接水方案,什么都不执行
    Observable=function(observer)\text{Observable} = \text{function}(\text{observer})
  2. 订阅触发\text{observer} \mapsto \text{subscription} 表示 subscribe 时才真正开闸,并返回订阅凭证
    subscription=observable.subscribe(observer)\text{subscription} = \text{observable.subscribe}(\text{observer})
  3. 推送数据\text{next}(x) 表示每次调用就把一条数据推进观察者,observer 收到后处理它
    next(x):observer↦receive(x)\text{next}(x) : \text{observer} \mapsto \text{receive}(x)

操作符管线:净水器流水线

光有水管还不够,后厨的水还得过滤、调味、限量。RxJS 的操作符(Operators)就是装在管线上的一节节「净水器」:map 像调味器,把每滴水变成另一种味道;filter 像滤网,把杂质剔掉;debounceTime 像限流阀,让水一滴一滴地流,防止客人疯狂点单把后厨冲垮。

操作符通过 pipe 方法串联,数据从源头依次流过每一节净水器,最后到达订阅者。转换类操作符有 map、pluck;过滤类有 filter、take、debounceTime;合并类有 merge、forkJoin、combineLatest;更高阶的 switchMap、mergeMap、concatMap 则处理「流里的流」——比如搜索框每输入一个字就发起一次请求。

其中 switchMap 特别值得记住:它只保留最新的内部流,新的请求一来,旧请求的订阅会被自动取消,完美解决搜索防抖后的竞态问题。forkJoin 则像等所有菜一起上桌,等全部源 Observable 完成后,把最后的结果打包成数组发给订阅者,类似 Promise.all。

把净水器一节节串起来,你就能把原始的事件洪流加工成业务需要的精致水流。调试 RxJS 代码时,像检查净水器一样逐个操作符排查,看数据在哪一环节被改、被滤、被延迟,很多诡异问题都会豁然开朗。

pipe(O1,O2,…,On)=On∘On−1∘⋯∘O1\displaystyle\text{pipe}(O_1, O_2, \dots, O_n) = O_n \circ O_{n-1} \circ \cdots \circ O_1
pipe 就是把一节节净水器串起来:数据先过第 1 节、再过第 2 节……最后到达水龙头(订阅者)。
  1. 操作符序列O_1, \dots, O_n 是流水线上安装净水器的顺序,先装的是最先处理数据的操作符
    source→O1S1→O2S2→O3⋯\text{source} \xrightarrow{O_1} S_1 \xrightarrow{O_2} S_2 \xrightarrow{O_3} \cdots
  2. 复合函数\circ 表示从右往左组合,数据先经过最左边的 O_1,再依次经过后面的操作符
    On∘On−1∘⋯∘O1O_n \circ O_{n-1} \circ \cdots \circ O_1
  3. 最终管道\text{pipe}(\cdot) 的输出仍是一个 Observable,对订阅者透明,他感知不到中间加了几节净水器
    pipe(O1,O2,…,On)=On∘On−1∘⋯∘O1\text{pipe}(O_1, O_2, \dots, O_n) = O_n \circ O_{n-1} \circ \cdots \circ O_1

实战应用:把智慧食堂开起来

现在,把智慧食堂真正开起来:门口是食客(用户),前台是组件,后厨是服务,订单管道是 RxJS 数据流。一个成熟的 Angular 项目,就是这套系统的高效运转。

在大型项目中,HTTP 请求天然是异步流,Angular 的 HttpClient 返回的正是 Observable,可直接 pipe 操作符加工,并在模板中用 async pipe 消费。对于跨组件的共享状态,常见做法是用 BehaviorSubject 保存最新状态,配合 switchMap 串联依赖请求,形成一个单向数据流的「总账本」。对比其他框架:React 使用单向数据流与 Hooks,异步生态常借助 RxJS 或状态库;Vue 内置响应式系统与 Pinia;Angular 则把 DI 与 RxJS 深度内建,风格更「全家桶」。

企业级实践还要注意:用 OnPush 变更检测提升性能、用拦截器统一处理 token 与错误、用注入 token 替代魔法字符串,让依赖关系一目了然。这些做法会让你的代码像一座运转良好的智慧食堂:食材由总管统一调配,订单由水管自动输送,每一环都清晰可控。

到这里,智慧食堂的蓝图已经完整:注入器是骨架,负责把食材送到每个档口;RxJS 是血脉,负责让订单与数据川流不息。掌握了 DI 与 RxJS,你就拿到了理解大型 Angular 项目的两张地图——一张看架构,一张看异步。

state=BehaviorSubject(s0),update:s↦f(s),view←async_pipe(state)\text{state} = \text{BehaviorSubject}(s_0), \qquad\text{update}: s \mapsto f(s), \qquad\text{view} \leftarrow \text{async\_pipe}(\text{state})
状态管理三件套:BehaviorSubject 是公告栏初值,update 是改写公告的纯函数,async pipe 让页面自动跟着公告变。
  1. 初始状态\text{BehaviorSubject}(s_0) 像公告栏一开始就贴着初值,任何新订阅者立刻能看到当前状态
    state=BehaviorSubject(s0)\text{state} = \text{BehaviorSubject}(s_0)
  2. 状态转移s \mapsto f(s) 表示每次用户操作通过纯函数产生新状态,旧状态不被修改
    update:s↦f(s)\text{update}: s \mapsto f(s)
  3. 单向数据流\text{view} \leftarrow \text{async\_pipe}(\text{state}) 表示视图只订阅状态流,模板随状态自动刷新
    view←async_pipe(state)\text{view} \leftarrow \text{async\_pipe}(\text{state})

AI 应用场景

大模型对话应用的流式输出

阶段:前端交互层 / 数据流处理

解决的问题:传统请求-响应模式下,要等整个回复生成完才能展示,体验卡顿;且用户连续提问会产生请求竞态。用 RxJS 把 SSE 流包装成 Observable,用 switchMap 保证只展示最新一轮回复,用 debounceTime 控制输入频次。

具体案例:某 AI 客服系统,用户每次提问后,后端以流式 token 返回。前端用 HttpClient 的 Observable 管道逐字渲染;用户输入时自动保存草稿,切换会话时取消旧订阅,避免旧回复覆盖新回复。

多模型服务切换的依赖注入架构

阶段:应用架构设计

解决的问题:同一套界面要对接多个大模型供应商(OpenAI、文心、通义),硬编码会导致替换服务时要改几十处。用 DI 的抽象 token 注册不同实现,通过环境配置切换 Provider,测试时注入 Mock 服务。

具体案例:某 AI 写作工具,定义了 LLMProvider 抽象接口,用 useFactory 依据配置文件决定注册哪个厂商客户端;单元测试时替换为假 LLM,保证确定性输出。

训练指标实时监控看板

阶段:模型训练运维

解决的问题:训练指标(loss、lr)通过 WebSocket 高频推送,若直接渲染会造成频繁抖动与消息挤压。用 RxJS 节流与过滤操作符把每秒几十条指标聚合成每秒一次展示,且只在数值变化明显时刷新图表。

具体案例:某 AI 团队的训练看板,WebSocket 消息进入 Subject,pipe bufferTime 聚合后交给 ECharts 渲染;断线重连时用 BehaviorSubject 保留最近指标,页面刷新不归零。

Python 演示

python64 行
"""A tiny DI container plus a reactive pipeline in Python."""  class Container:    def __init__(self):        self._providers = {}        self._singletons = {}     def register(self, token, factory, singleton=True):        self._providers[token] = (factory, singleton)     def resolve(self, token):        if token not in self._providers:            raise KeyError("provider not found: " + str(token))        factory, singleton = self._providers[token]        if not singleton:            return factory()        if token not in self._singletons:            self._singletons[token] = factory()        return self._singletons[token]  class Observable:    def __init__(self, producer):        self._producer = producer     def subscribe(self, on_next):        return self._producer(on_next)     def map(self, fn):        def producer(on_next):            return self._producer(lambda x: on_next(fn(x)))        return Observable(producer)     def filter(self, pred):        def producer(on_next):            return self._producer(lambda x: pred(x) and on_next(x))        return Observable(producer)  def from_list(items):    def producer(on_next):        for item in items:            on_next(item)        return lambda: None    return Observable(producer)  def main():    container = Container()    container.register("greeter", lambda: "hello from DI container")    print(container.resolve("greeter"))     source = from_list([200, 200, 500, 404, 200, 500])    pipeline = source.filter(lambda code: code >= 400).map(        lambda code: "error:" + str(code)    )    got = []    pipeline.subscribe(got.append)    print(got)  if __name__ == "__main__":    main()
目的:用 Python 复刻了「DI 容器 + Rx 式操作符」的最小原型,演示依赖注入如何装配服务、数据流如何经过 filter/map 加工出告警信息。
  1. self._providers[token] = (factory, singleton)把 token 与工厂函数登记到容器台账,相当于注册 Provider
  2. self._singletons[token] = factory()首次解析时缓存实例,实现单例,像总仓库的常备食材
  3. return self._producer(on_next)Observable 惰性执行的关键:订阅时才调用生产函数
  4. lambda x: on_next(fn(x))map 用包装函数逐个转换数据,像调味器给每滴水加味
  5. lambda x: pred(x) and on_next(x)filter 用短路求值实现「不满足条件就丢弃」,像滤网拦杂质
  6. pipeline.subscribe(got.append)订阅后数据流开始流动,收集所有被加工过的结果

常见误区

误区我一直以为只要写了 `providedIn: 'root'`,整个应用里这个服务就只有一个实例,无论在哪里注入都是同一个对象。

正解providedIn: 'root' 只表示服务注册在根注入器,全局共享同一实例;但如果你在某个组件的 providers 里重新声明(覆写)了这个 token,该组件及其子树会拿到新实例——就像档口冰柜里放了自己的特供食材,优先级高于总仓库。

为何常见:配置项名字里有 root,容易让人误以为「root 即全局、全局即唯一」,忽略了注入器树的可覆写特性。

误区我以为 `new Observable(...)` 一执行,数据就会自动流出来,用的时候直接接就行,不需要 subscribe。

正解Observable 是惰性的:创建时只是保存了一张「接水方案」,必须调用 subscribe 才会执行生产逻辑并开始发射数据;每次 subscribe 都会重新执行一遍(冷流)。而 Promise 创建后立即执行,二者的区别就像「水管接好」与「水龙头打开」。

为何常见:Promise 先入为主,让人默认所有异步对象都是「创建即执行」,没意识到订阅才是开闸的动作。

误区我担心一切订阅都泄漏,所以连 async pipe 也自己在 ngOnDestroy 里又 unsubscribe 了一遍。

正解async pipe 由 Angular 自动管理订阅与销毁,模板销毁时它自动取消订阅,不需要也不建议手动再退订一次。真正需要手动 unsubscribe 的是组件里直接 subscribe 的长生命周期源,例如 interval、Subject、路由参数流。

为何常见:被「手动订阅必须手动释放」的教条吓到了,没区分框架托管与自定义订阅两种场景。

误区我以为 map 就是把每条数据改一下,filter 就是把不要的删掉,跟数组的 map/filter 差不多,返回的还是一个数组。

正解RxJS 操作符作用的是「流」而不是数组:它们返回的是一个新的 Observable,数据随时间逐条流动。数组方法一次处理完一整批;操作符逐条处理、且有时间维度,还支持 debounceTime、switchMap 这类数组没有的时序能力。

为何常见:JavaScript 里数组的 map/filter 太常见,望文生义地套用,忽略了「流」的时间维度。

面试题

Q1. 关于 Angular 中依赖注入的作用,下列说法正确的是?

单选题 · easy

  • A.组件通过构造函数自动获得依赖实例← 正确答案
  • B.组件必须自己 new 出依赖对象
  • C.依赖注入只能服务于类的注入
  • D.依赖注入仅用于发送 HTTP 请求

答案:A。依赖注入的核心正是「组件不在内部 new 依赖,而是由框架注入」,A 描述的就是构造函数注入的标准效果。B 是自己 new,正是 DI 要消除的反模式;C 错在 DI 对指令、管道等一切可注入类都适用;D 错在 DI 管理所有依赖,不只是 HTTP。像厨师喊单,由配菜总管送料。

Q2. @Injectable({ providedIn: 'root' }) 的含义是?

单选题 · mid

  • A.只能在根组件中使用该服务
  • B.该服务由根注入器提供,全应用共享← 正确答案
  • C.该服务必须手动在模块中注册
  • D.该服务每次注入都创建新实例

答案:B。providedIn: 'root' 表示该服务注册在根注入器,整个应用共享同一实例,所以 B 正确。A 错在 root 不代表只能在根组件用,而是处处可注入;C 错在 providedIn 注册后无需在模块手动声明;D 错在共享单例正是默认行为,需要新实例应在组件 providers 中覆写。如同总仓库的常备食材,全食堂共享。

Q3. 关于 Observable 的行为,以下说法正确的是?

单选题 · mid

  • A.创建后立即开始发射数据
  • B.必须调用 subscribe 后才会执行← 正确答案
  • C.与 Promise 行为完全相同
  • D.只能发射一条数据且立即结束

答案:B。Observable 是惰性对象,创建时只保存生产逻辑,只有 subscribe 才真正执行并发射数据,B 正确。A 描述的是 Promise 的即时执行行为;C 错在 Observable 可发射多值、可取消;D 错在流可以发射无数条数据。好比水管接好后不会自动流水,打开水龙头(订阅)才出水。

Q4. BehaviorSubject 与普通 Subject 的关键区别是?

单选题 · easy

  • A.BehaviorSubject 自带初始值并可重放最近一次值← 正确答案
  • B.BehaviorSubject 与 Subject 的使用方式完全相同
  • C.BehaviorSubject 创建后必须立即手动订阅
  • D.BehaviorSubject 只能用于异步的数据流中

答案:A。BehaviorSubject 必须提供初始值,并会把最近一次值重放给新订阅者,这正是「公告栏永远显示最新公告」的机制,A 正确。B 错在它比 Subject 多出初始值与重放能力;C 错在它同样惰性、仍需订阅;D 错在它适用任意数据流场景。

Q5. 在组件树中,子组件解析依赖的查找顺序是?

单选题 · hard

  • A.先找根注入器再查找组件注入器
  • B.先找自身注入器再沿父级向上查找← 正确答案
  • C.只查找模块级共享的注入器
  • D.直接抛出依赖未找到的异常

答案:B。Angular 注入器呈树状,组件解析依赖时先查自身注入器(本级 providers),再沿父级链向上直到根注入器,B 正确。A 把顺序搞反了;C 忽视了组件级注入器;D 错在只有全部查找失败后才报错。如同档口先翻自己冰柜,没有才向总仓库调货。

Q6. 下列哪些操作符属于 RxJS 过滤类操作符?

多选题 · mid

  • A.filter 过滤类条件筛选操作符← 正确答案
  • B.debounceTime 防抖节流类操作符← 正确答案
  • C.map 转换类映射操作符
  • D.forkJoin 合并类组合操作符

答案:AB。filter 按条件保留通过的数据,debounceTime 限流防抖,二者都属于过滤类,答案 [0,1]。map 是转换类,forkJoin 是合并类,它们不负责筛选数据。可以类比净水器:滤网与限流阀都装在过滤环节。

Q7. switchMap 与 mergeMap 的主要区别是?

单选题 · mid

  • A.switchMap 取消旧订阅只保留最新内层流← 正确答案
  • B.mergeMap 不会取消旧的内层订阅流
  • C.两者在行为与用法上没有区别
  • D.switchMap 不能处理异步任务流

答案:A。switchMap 每来一个新外层值,就取消上一个内部订阅并创建新订阅,因此只保留最新内层流,A 正确。B 说的是 mergeMap 的保留行为,但这描述的是「不取消」,与题干问的 switchMap 特点不符;C 明显错误;D 错在 switchMap 处理异步竞态正是它的强项。如同换频道,只播最新频道的声音。

Q8. 关于 Angular 层级注入器,下列说法正确的是?

多选题 · hard

  • A.组件级 providers 会创建独立实例← 正确答案
  • B.注入器呈树状结构并向上查找← 正确答案
  • C.providedIn 为 root 时无法覆写
  • D.模块级与组件级可配置 provider← 正确答案

答案:ABD。组件级 providers 会创建独立注入器与实例,A 正确;注入器沿组件树向上查找,B 正确;providedIn: 'root' 服务的 token 可以在组件或模块的 providers 中覆写,C 错误;模块级与组件级都可配置 provider,D 正确。这就像档口冰柜可以放特供食材来覆盖总仓库的标准料。

Q9. async pipe 在处理订阅时的作用是?

单选题 · easy

  • A.自动订阅与销毁避免内存泄漏← 正确答案
  • B.手动管理所有订阅对象
  • C.把 Observable 直接转成数组
  • D.只能用于 HTTP 请求结果

答案:A。async pipe 在模板中自动订阅 Observable,并在组件销毁时自动取消订阅,避免内存泄漏,A 正确。B 与 async pipe 的自动化设计矛盾;C 错在它保持流式渲染而非转数组;D 错在它适用于任意数据流。好比自动水龙头,人走水停。

Q10. 关于 RxJS 内存泄漏,正确的说法是?

多选题 · hard

  • A.长生命周期源需手动取消订阅← 正确答案
  • B.async pipe 会自动处理订阅销毁← 正确答案
  • C.每次订阅后必须调用 next 方法
  • D.组件销毁时调用 unsubscribe 即可← 正确答案

答案:ABD。interval、Subject 等长生命周期源必须手动取消订阅,A 正确;async pipe 由框架托管销毁,B 正确;subscribe 后无需调用 next,next 是数据源发射数据的方法,C 错误;组件销毁时调用 unsubscribe 可释放订阅,D 正确。