组件只是带有模板的指令? 还是?
从我开始使用Angular的那一刻起,我就被这个问题所吸引,这个问题是组件和指令之间的区别是什么。 对于那些来自AngularJS世界的人来说,这是一个令人困扰的问题,因为我们只有经常用作组件的指令。 如果您在网上搜索解释,将会看到许多类似以下的短语:
组件只是在模板中定义了内容的指令。
角组件是一个子集 指令。 与指令不同,组件始终具有……
组件是带有模板的高阶指令,并用作…
哪个都很好,但是真正的问题是如何 ? 组件如何成为指令? 我认为您不会找到这样的解释,因为要提供这种解释,必须对Angular内部的工作方式有充分的了解。 如果这个问题困扰了您一段时间,那么本文适合您。 目的是揭开谜底。 但是准备一些硬核东西stuff。
好老式的 看法
如果您已经阅读了我以前的文章,特别是Angular如何更新DOM ,您可能已经知道Angular应用程序是一棵视图树。 每个视图都是从工厂生成的,由不同类型的视图节点组成,每个节点都有特定的功能。 在提到的文章中(这将大大有助于理解本文),我展示了两种最简单的节点类型-元素定义和文本定义。 前者是为所有元素DOM节点创建的,后者是为所有文本节点生成的。
因此,如果您有这样的模板:
<div><h1>Hello {{name}}</h1></div>
编译器将使用两个div和h1 DOM元素的元素节点以及一个Hello {{name}}部分的文本节点生成视图定义。 这些是非常重要的节点,因为没有它们,我们将无法在屏幕上看到任何内容。 但是由于组件组成模式指示我们应该能够嵌套组件,因此对于嵌入式组件,必须有另一种视图节点。 为了弄清楚那些特殊的节点是什么,让我们首先看看该组件是由什么组成的。 组件本质上是具有在组件类中实现的附加行为的DOM元素。 让我们从DOM元素开始。
自定义DOM元素
您可能知道,您可以创建一个新的HTML标记并在html中使用。 例如,如果您不使用任何框架,而是将以下内容插入html:
<a-comp></a-comp>
然后查询DOM节点并检查其类型,您将看到它是一个完全有效的DOM元素:
const element = document.querySelector('a-comp'); element.nodeType === Node.ELEMENT_NODE; // true
这个a-comp元素将由浏览器使用继承自HTMLElement接口的HTMLUnknownElement接口创建,但无需实现任何其他属性或方法。 您将能够使用CSS设置样式,并将事件侦听器附加到常见事件(例如click)。 如我所说,完美的html元素。
如果将其变成自定义元素,则可以创建该元素的升级版本。 您将需要为其创建一个类并使用提供的API进行注册:
class AComponent extends HTMLElement {...} window.customElements.define('a-comp', AComponent);
它看起来类似于您一段时间以来所做的事情吗?
是的,这与我们在定义组件时在Angular中所做的非常相似。 实际上,Angular非常严格地遵循了Web组件规范,但为我们简化了很多事情,因此我们不必自己创建影子根并将其附加到host元素。 但是,我们在Angular中创建的组件并未注册为自定义元素,而是由框架以非常特定的方式处理的。 如果您想知道如何在没有任何框架的情况下创建组件,请阅读《 自定义元素v1:可重用的Web组件》 。
好的,我们已经知道可以创建任何HTML标记并在模板中使用它。 因此,如果我们在Angular的组件模板中使用它,框架将为此标签创建一个元素定义就不足为奇了:
function View_AppComponent_0(_l) { return jit_viewDef2(0, [ jit_elementDef3(0, null, null, 1, 'a-comp', [], ...) ]) }
但是,您必须通过向模块或组件装饰器属性添加以下架构来向Angular表示您正在使用自定义元素:[CUSTOM_ELEMENTS_SCHEMA]到模块或组件装饰器属性,否则Angular编译器将生成错误:
'a-comp' is not a known element: 1. If 'c-comp' is an Angular component, then ... 2. If 'c-comp' is a Web Component then add...
因此,我们有一个元素,但缺少该类。 Angular中除了组件之外还具有类吗? 当然,有一个指令! 让我们添加一个指令,看看最终结果。
指令定义
您可能知道每个指令都有一个选择器,该选择器可用于定位特定的DOM元素。 大多数指令使用属性选择器,但是元素选择器也很好。 实际上,Angular表单指令使用元素选择器表单将特定行为隐式附加到html表单。
因此,我们可以创建不执行任何操作的指令并将其应用于我们的自定义元素。 让我们这样做,看看视图定义是什么样的:
@Directive({selector: 'a-comp' }) export class ADirective {}
现在让我们检查一下工厂:
function View_AppComponent_0(_l) { return jit_viewDef2(0, [ jit_elementDef3(0, null, null, 1, 'a-comp', [], ...), jit_directiveDef4(16384, null, 0, jit_ADirective5, [],...) ], null, null); }
好的,所以现在编译器将新的jit_directiveDef4节点与元素定义一起添加到视图定义中。 它还将元素定义的childCount参数设置为1,因为应用于元素的所有指令都被视为该元素的子元素。
新添加的指令定义是由directiveDef函数生成的非常简单的节点定义。 它采用以下参数:
+----------------+-------------------------------------------+ | Name | Description | +----------------+-------------------------------------------+ | matchedQueries | used when querying child nodes | | childCount | specifies how many children | | | the current element have | | ctor | reference to the component or | | | directive constructor | | deps | an array of constructor dependencies | | props | an array of input property bindings | | outputs | an array of output property bindings | +----------------+-------------------------------------------+
出于本文的目的,我们仅对ctor参数感兴趣。 这只是对我们为指令定义的ADirective类的引用。 当Angular创建指令实例时(我很快会写这个,所以请务必跟随我😃),它将在此处实例化指令类。 然后,它将作为提供者数据存储在视图节点上。
好的,所以我们的实验表明组件只是元素和指令定义。 就是这样吗 您可能知道,Angular并非总是那么简单。
代表组件
上面已经显示了如何通过创建自定义HTML元素和以该元素为目标的指令来模拟组件。 现在让我们定义一个真实的组件,并将生成的工厂与我们在实验中获得的工厂进行比较:
@Component({ selector: 'a-comp', template: '<span>I am A component</span>' }) export class AComponent {}
准备好进行比较了吗? 这是生成的工厂:
function View_AppComponent_0() { return jit_viewDef2(0, [ jit_elementDef3(0, null, null, 1, 'a-comp' , [], ... jit_View_AComponent_04, jit__object_Object_5),
jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...)
好的,所以我们只是确认了前面几章的内容。 实际上,Angular将组件表示为两个视图节点-一个元素和一个指令定义。 但是,当使用实际组件时,参数列表与元素和指令定义节点之间存在一些差异。 让我们来探索它们。
节点标志
节点标志是所有节点定义的第一个参数。 它实际上是节点标志的位掩码,其中包含框架在更改检测周期主要使用的特定节点信息。 在两种情况下,此数字都不同:16384-用于简单指令,而49152用于组件指令。 要了解编译器设置了哪些标志,我们将数字简单地转换为二进制形式:
16384 = 100000000000000 // 15th bit set 49152 = 1100000000000000 // 15th and 16th bit set
如果您对转换的方式有好奇,请阅读十进制二进制转换算法后面的简单数学 。 因此,对于简单的指令,编译器仅设置第15位,这在Angular源代码中是这样完成的:
TypeDirective = 1 << 14
并为组件节点设置了第15位和第16位,即
TypeDirective = 1 << 14 Component = 1 << 15
现在应该清楚为什么数字不同。 为指令生成的节点标记为TypeDirective节点,为组件指令生成的节点另外标记为Component。
查看定义解析器
由于a-comp现在是具有以下简单模板的组件:
<span>I am A component</span>
编译器会为其生成一个带有自己的视图定义和视图节点的工厂:
function View_AComponent_0(_l) { return jit_viewDef1(0, [ jit_elementDef2(0, null, null, 1, 'span', [], ...), jit_textDef3(null, ['I am A component'])
角度视图是一棵视图树,因此父视图定义需要引用子视图定义。 子视图定义存储在为组件生成的元素节点上。 在我们的例子中,为a-comp生成的元素定义节点将保存a-comp的视图。 a-comp元素节点接收的jit_View_AComponent_04参数是对代理类的引用,该代理类将解析将创建视图定义的工厂。 每个视图定义仅创建一次,然后存储在DEFINITION_CACHE上 。 然后,当Angular 创建视图实例时,将使用此视图定义。
组件渲染器类型
Angular根据组件装饰器中指定的ViewEncapsulation模式使用多个DOM渲染:
组件的渲染器由DomRendererFactory2类创建。 在定义内部传递的参数componentRendererType(在我们的示例中为jit__object_Object_5)基本上是需要为组件创建的渲染器的描述符。 它包含的最重要的信息是视图封装模式和需要应用于组件视图的样式:
{ styles:[["h1[_ngcontent-%COMP%] {color: green}"]], encapsulation:0 }
如果为组件定义任何样式,则编译器会自动将组件的封装模式设置为ViewEncapsulation.Emulated。 或者,您可以使用封装组件装饰器属性显式指定模式。 如果您未设置任何样式并且未为组件指定封装模式,则描述符定义为ViewEncapsulation.Emulated ,实际上被忽略 。 具有此类描述符的组件将使用父组件渲染器。
子指令
现在,剩下的最后一条信息是,如果我们将指令应用于模板中的组件,将生成以下内容:
<a-comp adir></a-comp>
我们已经知道,当为AComponent生成工厂时,编译器将为a-comp HTML元素创建元素定义,并为AComponent类创建指令定义。 但是由于编译器会为每个指令生成指令定义节点,因此上述模板的工厂将如下所示:
function View_AppComponent_0() { return jit_viewDef2(0, [ jit_elementDef3(0, null, null, 2, 'a-comp' , [], ... jit_View_AComponent_04, jit__object_Object_5), jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...) jit_directiveDef6(16384, null, 0, jit_ADirective8, [], ...)
它包含了我们从未见过的任何东西。 仅添加了一个指令定义,并且该元素的子代计数增加到2。
而已。 😓!
您发现文章中的信息有帮助吗?
From: https://hackernoon.com/here-is-why-you-will-not-find-components-inside-angular-bdaf204d955c