先记住这个答案
在Angular中,服务实例的共享范围取决于providers定义的位置。在@Injectable({providedIn:'root'})或根模块providers注册时,全局共享单例;在组件元数据的providers中注册时,每个组件实例会创建自己的服务实例,并对其子组件可见(除非子组件有自己的providers)。子组件的providers会覆盖父级,导致该子组件及其子树使用新的实例。因此,作用域本质上是沿着组件树向下传播的注入器层级决定的。
- 根注入器提供全局单例服务。
- 组件providers为每个组件实例新建服务。
- 子组件providers遮蔽父级同令牌。
注入器树如何决定实例作用域
Angular应用是一个分层注入器树,每个组件实例都有与之关联的注入器。当在组件元数据中声明providers时,Angular为该组件实例创建一个新的注入器节点,并将服务实例挂载在该节点上。服务解析时,Angular从当前组件的注入器开始向上查找,直到找到对应的provider。因此,providers定义在哪一层,实例就归属于哪一层。
根模块或providedIn:'root'的服务注册在根注入器,全局唯一。而组件级providers则使该组件每次实例化时都会获得一个全新的服务实例,这个实例只对该组件及其子组件可见,不会影响兄弟组件或父组件。若子组件也声明了同令牌的providers,则子组件及其子树将使用子组件提供的实例,形成覆盖。
购物车状态在多个组件间独立与共享的控制
假设有一个ShoppingCartService,用于存储购物车条目。在App模块中通过providers注册,则所有组件共享同一购物车。若在ProductListComponent中通过providers注册,而ProductListComponent被用在两个路由出口中,则每个路由实例会创建独立的购物车,导致用户在不同页面切换时购物车内容丢失。
为了保留用户在不同页面间的购物车数据,应使用根级provider。但若某个组件(如AdminCartPreview)希望独立于全局购物车显示临时修改,可在其providers中注册该服务,这样它创建自己的实例,不影响全局。决策时需权衡数据共享需求:需要跨路由共享则用根级,仅局部临时则用组件级。
失效条件与处理代价
组件providers创建的服务实例会随其组件注入器的销毁而销毁。若服务实现了ngOnDestroy,注入器销毁时会调用它;但Angular不会自动清理服务持有的外部资源,因此应在服务的ngOnDestroy中显式清理,避免内存泄漏。
开发者常误以为子组件的providers会影响兄弟组件,实际上作用域是树状向下。另外,惰性加载模块会创建新的模块注入器,若只在模块providers中注册服务,则该服务仅在该模块内共享,与其他模块不同实例。要全局共享,必须使用根providedIn或根模块providers。
容易答错的地方
- 认为组件providers对该组件所有实例共享
- 组件providers中的服务不是组件类级共享,而是每个组件实例独立。若同一组件在模板中显示多次,每次都会有自己的服务实例。这会导致状态不共享,可能引发bug。
- 混淆providedIn和NgModule的providers
providedIn:'root'与根模块providers等效,但模块级providers(在惰性加载模块中)会创建独立注入器,导致不同模块中的服务实例不共享。需注意使用场景。
面试官还会怎么问?
组件providers中服务实例何时销毁?
当组件及其子组件被销毁时,Angular会销毁对应的组件注入器,并调用由该注入器创建且实现了ngOnDestroy的服务钩子。若没有其他引用,服务实例随后可被垃圾回收;若服务持有外部资源,应在服务的ngOnDestroy中显式清理。
如何强制服务在每个组件中独立但可延迟初始化?
在组件providers中提供服务的工厂函数,确保每次组件实例化时创建新实例。需管理好依赖。也可使用useFactory动态创建。
惰性加载模块的providers是否会被隔离?
是。每个惰性加载模块有独立注入器,其providers中的服务只在模块内共享。若需全局,放在根注入器(如providedIn:'root')。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。