这篇文章,我们会研究一个新的Angular Injector,叫作NodeInjector,它使用了布隆过滤器来检索令牌。我们将介绍以下三个方面:
- NodeInjector的结构
- Angular布隆过滤器是如何构建的
- NodeInjector是怎么来检索依赖的
介绍
Angular DI有两种Injector,NodeInjector和R3Injector。R3Injector就是Angular文档Hierarchical injectors部分提到的EnvironmentInjector,NodeInjector对应的是ElementInjector,他们是Angular内部实现Environment/ElementInjector概念的两个类。
NodeInjector由Angular新的渲染引擎Ivy引入。
NodeInjector取代了当前的ElementInjector(上图中的 Injector_),并通过使用布隆过滤器来减少 Angular 应用程序中的内存压力。
让我们来看一个简单的例子
@Component({
selector: 'my-app',
template: `
<comB></comB>
`
})
export class AppComponent {}
@Component({
selector: 'comB',
template: '<div>sample</div>'
})
export class ComB {
constructor(private rootComp: AppComponent) {}
}
这个例子很简单,我们有一个根组件Appcomponent,它的模版使用了comA组件。
我们将要了解的是 Angular 如何在 comA 组件中获取根 AppComponent 实例。 现在我们已经明确了目标,让我们开始吧。
模版视图
我想你对 Angular 中的视图对象的概念有所熟悉。简单来说,它是表示 Angular 模板的一些内部对象。
我们知道初始化应用时,Angular解析模版,构建视图树(等同于组件树)。Angular使用LView
和TView
内部对象来表示这颗视图树,想进一步了解Angular Ivy视图树的,可以参考Ivy’s internal data structures。
LView
数组包含描述特定模板的数据,并且在 TView.data
数组中 Angular 保留了跨模板共享的信息。简单来说,一个存储动态数据,一个存储静态数据。Angular把动静态数据分离,减少内存重复占用。可以阅读View Data Explanation来细致学习下Angular View保存的数据。
此外,Angular Ivy 会把节点的注入信息存储在视图的数据里。它在 LView
和 TView.data
中为每个Injector分配插槽,这些插槽用来表示两种布隆过滤器:累积布隆过滤器和模板布隆过滤器。一个视图有多少Injector,就有多少组布隆过滤器。
下面是它的可视化效果
解释下这张图
- slot0到24是Header部分,包含有关模板的上下文信息。其中slot9存放了一个指向父Injector的指针,这是Angular检索依赖算法切换到EnvironmentInjector的地方。
- LView和TView.data数组包含长度为8个槽位([n, n + 7] 个索引)的布隆过滤器,每个槽位有32个位,即一个JS整数的长度。
- 布隆过滤器在 parentLocation 槽(n + 8 索引)中都有一个指向父布隆过滤器的指针。
- Angular将所有token(图上的[view]provider types)存储在 TView.data 中,并将实例([view]provider instances)存储在 LView 中,以便Angular在检索provider时找到实例。
让我们回到我们的应用程序,看看有多少个视图:
我们有一个Root View, AppComponent View和ComB View。他们分别有一对布隆过滤器(在LView 上的cumulative布隆过滤器,和在TView.data上的template布隆过滤器)。
slot14的declaration View指针建立declaration层面的父子关系,子级cumulative步隆过滤器的创建是合并父级的cumulative和template布隆过滤器,这个过程需要通过declaration View指针来查找父级过滤器。比如CombB LView的cumulative布隆过滤器(slot42-39)是AppComponnet LView的cumulative与AppComponent TView.data的template布隆过滤器的合并(位操作异或)。同理AppComponent LView的cumulative布隆过滤器是Root View上两个过滤器的合并。
parentLocation槽位是指向父布隆过滤器的指针,它不仅包含父Injector的索引,还有declaration View的偏移量declarationViewOffset,通过这两个值可以准确地找到父布隆过滤器。比如ComB的parentLocation(slot 40)的值43300是父Injector索引,即AppComponent View的slot 30,和declaration View的偏移量1(因为ComB View的declaration View指针指向的AppComponent View存在布隆过滤器)的打包。
现在我们已经了解了View,接下来我们来看下布隆过滤器。
cumulative和tempate布隆过滤器
布隆过滤器是一种查找算法,有点像hash table的变种,但它不保存值,只是告知你是否存在。如果你不熟悉布隆过滤器,可以阅读下Probabilistic Data structures: Bloom filter和Bloom Filters by Example。
Anuglar Ivy有一个有趣的布隆过滤器实现。我们首先来了解下template布隆过滤器。
temaplate布隆过滤器是一种保存当前节点的令牌(token)信息,它位于Tview.data上。我们来学习下它是怎么构建的。如果你阅读了上面了解布隆过滤器的文章,那应该知道布隆过滤器的基本数据结构是位向量。Angular Ivy 布隆过滤器长度定义为256,就是说是一个256位的向量,它被分成 8 个bucket,每个部分32位。
Angular如何对过滤器元素做Hash
首先,Angular 通过递增整数值为 token 生成一个唯一 ID(如果还没定义),并将其放入静态 NG_ELEMENT_ID 属性中。
我们上面例子中的AppComponent的ID是0,因为token生成从0开始,并且 AppComponent 是 Angular 添加到依赖注入系统的第一个组件。
然后,它用这个ID与布隆过滤器的长度做按位与(&)运算,得到的结果始终在0-255之间。
下一步,创建一个与指令相关的特定位的掩码。JS 位操作是 32 位,因此这将是 2^0 和 2^31 之间的数字,对应于 32 位整数中的位位置 0 - 31。
最后,用bloomHash除以32(一个bucket的位长度)并向下求整,求得目的bucket,把掩码设置进去。不过,这里是通过右移位运算来做的除法。
现在我们来看一个简单的例子,我们有一个布隆过滤器,它的结构如下:
假设有一个directive,它的__NG_ELEMENT_ID__等于1。
哈希ID并存储到布隆过滤器。
Ivy 如何检查给定的 Id 是否在布隆过滤器中呢? Ivy 根据Id创建相同的掩码,并找到对应的存储bucket,与该掩码进行匹配检查(通过按位与操作)。
现在,我们来谈谈cumulative布隆过滤器。正如上面讨论子级cumulative步隆过滤器时提到的,它本质上是将父节点的cumlative和template布隆过滤器合并。使用这个过滤器,我们可以快速判断父Injector中是否存在token,而无需遍历所有父Injector。
NodeInjector 是什么?
NodeInjector是一种属于某个Node(directive或者component)的Injector,像R3Injector一样,是一个用于检索Provider定义的对象实例。但它又是一种有别于R3Injector的特殊容器,因为它把Provider实例存放在View上。
NodeInjector保存Provider实例在哪儿?
首先,我们在源码中看下它的定义
与R3Injector比较下
我们看到NodeInjector没有像R3Injector一样的key-value存储。不过,它引用了TNode和LView,NodeInjector通过查看 TNode 和 LView 对象中包含的数据来获取所需的Provider实例。
这些数据就是之前章节讨论的布隆过滤器,Angular在TNode上创建了一个InjectorIndex属性,以便知道该节点对应的布隆过滤器位于LView的哪个位置。另外,正如我们之前所了解的,在每个布隆过滤器之后,Angular还将parentLocation存储在LView数组中,使得Angular可以遍历所有父Injector。
检索依赖算法
如我们之前看到的,NodeInjector的get方法是检索Provider实例的开始点
如代码所示,getOrCreateInjectable 方法是主要的入口点。简单分析下,首先查找嵌入视图Injector, 再查找NodeInjector,没有找到的情况下,回退到moduleInjector。这里我们主要来看下查找NodeInjector的过程。代码比较长,我来给你分析一下。
假设我们正在调用Injector.get(SomeClass)
- Angular 会在 SomeClass.NG_ELEMENT_ID 静态属性中查找哈希值。
- 如果该哈希值是一个工厂函数,我们应该通过调用该函数来初始化对象。
Angular 在 NG_ELEMENT_ID 静态属性中为以下特殊对象定义了工厂函数:ChangeDetectorRef、ElementRef、TemplateRef、ViewContainerRef、Renderer2。
- 如果该哈希值是一个数字类型,我们会:
- 从TNode获取InjectorIndex的值。
- 检查Template布隆过滤器是否有该哈希值(通过TView.data[injectorIndex]找到template布隆过滤器的位置)。
- 如果Template布隆过滤器告诉我们结果为真,那么我们根据SomeClass token找到该token在TView.data中的索引Index,再通过Lview[index]找到依赖实例。
- 如果结果为假,那么我们看下cumulative布隆过滤器,是否父View上有该依赖。
- 如果cumulative布隆过滤器告诉我们结果为真,则在重复第二步,检查父Template布隆过滤器是否有该哈希值,如此循环。
- 如果最终找到,则返回依赖实例。
- 如果没有,则切换到 ModuleInjector查找依赖。
现在是时候看下,在我们的例子中是如何获取根 AppComponent
总结
我们介绍了Angular View的结构,View上的布隆过滤器,以及NodeInjector查找依赖的检索算法。希望你对Angular NodeInjector是怎么工作的有了一个基本的认识。理解他们对一些技术场景会非常有帮助,比如我在最近的Angular 微前端架构设计中,对这块知识有很大的依赖。对他们的预先掌握,让我顺利完成了微前端的项目实践。