MST

星途 面试题库

面试题:深度剖析Angular依赖注入的底层实现及优化策略

请深入阐述Angular依赖注入底层是如何实现对象的创建、查找和注入的。在大型项目中,依赖注入可能会带来性能问题,针对这些潜在问题,有哪些优化策略和最佳实践?
27.6万 热度难度
前端开发Angular

知识考点

AI 面试

面试题答案

一键面试

Angular依赖注入底层实现

对象的创建

  1. 注入器(Injector):Angular中的注入器是一个负责创建和管理对象实例的容器。每个注入器都维护着一个依赖映射表,这个表记录了每个令牌(token,通常是一个类、字符串或符号)与其对应的创建逻辑。
  2. 工厂函数:当注入器需要创建一个对象时,它会查找依赖映射表。如果找到的是一个工厂函数,注入器会调用这个工厂函数,并将所需的依赖作为参数传递进去,从而创建对象实例。例如:
const myFactory = (dependency1, dependency2) => {
  return new MyClass(dependency1, dependency2);
};
providers: [
  {
    provide: MyClass,
    useFactory: myFactory,
    deps: [Dependency1, Dependency2]
  }
]
  1. 类构造函数:如果依赖映射表中直接注册的是一个类,注入器会使用new关键字调用该类的构造函数来创建对象实例。同时,注入器会递归地解析构造函数参数的依赖,并创建这些依赖的实例。例如:
class MyClass {
  constructor(private dependency: Dependency) {}
}
providers: [MyClass, Dependency]

对象的查找

  1. 注入器树:Angular应用是基于注入器树来管理依赖的。根注入器位于树的顶端,每个组件可以拥有自己的子注入器。当一个组件需要查找某个依赖时,它首先在自己的注入器中查找。如果找不到,会沿着注入器树向上查找,直到根注入器。
  2. 令牌匹配:注入器通过令牌来查找依赖。令牌可以是一个类、字符串或符号。例如,如果一个组件需要注入MyService,注入器会查找与MyService类对应的实例。如果使用字符串或符号作为令牌,注入器会查找匹配该字符串或符号的实例。

对象的注入

  1. 构造函数注入:这是最常见的注入方式。在类的构造函数中声明依赖,注入器会在创建该类实例时,将相应的依赖实例传递给构造函数。例如:
class MyComponent {
  constructor(private myService: MyService) {}
}
  1. 属性注入:可以使用@Inject装饰器在类的属性上进行注入。虽然这种方式不太常用,但在某些情况下很有用。例如:
class MyComponent {
  @Inject(MyService) myService: MyService;
}
  1. 方法注入:相对较少使用,通过在类的方法参数上使用@Inject装饰器来注入依赖。例如:
class MyComponent {
  myMethod(@Inject(MyService) myService: MyService) {
    // 使用myService
  }
}

大型项目中依赖注入性能问题及优化策略

潜在性能问题

  1. 创建开销:在大型项目中,可能会有大量的依赖需要创建,每个依赖的创建都可能涉及到构造函数执行、依赖解析等操作,这会带来一定的性能开销。
  2. 注入器查找开销:由于注入器树的存在,当在组件树较深位置查找依赖时,可能需要遍历多层注入器,这会增加查找时间。
  3. 循环依赖:如果不小心创建了循环依赖,会导致注入器在解析依赖时陷入无限循环,最终导致应用崩溃。

优化策略和最佳实践

  1. 延迟加载:对于一些不常用的依赖,可以使用延迟加载的方式。在Angular中,可以通过路由的loadChildren属性来实现模块的延迟加载,从而延迟相关依赖的创建。例如:
const routes: Routes = [
  {
    path: 'feature',
    loadChildren: () => import('./feature.module').then(m => m.FeatureModule)
  }
];
  1. 单例模式:对于一些全局共享的服务,确保它们在整个应用中只有一个实例。Angular默认在根注入器中注册的服务是单例的。如果需要在组件级别也保持单例,可以使用providedIn: 'root'来注册服务。例如:
@Injectable({
  providedIn: 'root'
})
class MySingletonService {}
  1. 减少注入器层次:尽量减少注入器树的深度,避免不必要的子注入器。可以将一些通用的依赖放在较高层次的注入器中,减少查找开销。
  2. 检测循环依赖:在开发过程中,使用工具(如ng lint)来检测潜在的循环依赖。如果发现循环依赖,需要重新设计依赖关系,打破循环。
  3. 缓存创建结果:对于一些创建开销较大且不会经常变化的依赖,可以手动实现缓存机制,避免重复创建。例如,可以在工厂函数中添加缓存逻辑:
let myInstance;
const myFactory = () => {
  if (!myInstance) {
    myInstance = new MyClass();
  }
  return myInstance;
};
providers: [
  {
    provide: MyClass,
    useFactory: myFactory
  }
]