[译] 为什么 Angular 内部没有发现组件

原文连接: Here is why you will not find components inside Angular

Component Not Found

Component is just a directive with a template? Or is it?

从我开始使用 Angular 开始,就被组件和指令间区别的问题所困惑,尤为对那些从 Angular.js 世界来的人,由于 Angular.js 里只有指令,尽管咱们也常常把它当作组件来使用。若是你在网上搜这个问题解释,不少都会这么解释(注:为清晰理解,不翻译):javascript

Components are just directives with a content defined in a template…

Angular components are a subset of directives. Unlike directives, components always have…html

Components are high-order directives with templates and serve as…java

这些说法貌似都对,我在查看由 Angular 编译器编译组件生成的视图工厂源码里,的确没发现组件定义,你若是查看也只会发现 指令node

注:使用 Angular-CLI ng new 一个新项目,执行 ng serve 运行程序后,就可在 Chrome Dev Tools 的 Source Tab 的 ng:// 域下查看到编译组件后生成的 **.ngfactory.js 文件,该文件代码即上面说的视图工厂源码。

可是我在网上没有找到 缘由解释,由于想要知道缘由就必须对 Angular 内部工做原理比较熟悉,若是上面的问题也困让了你很长一段时间,那本文正适合你。让咱们一块儿探索其中的奥秘并作好准备吧。git

本质上,本文主要解释 Angular 内部是如何定义组件和指令的,并引入新的视图节点定义——指令定义。github

注:视图节点还包括元素节点和文本节点,有兴趣可查看 译 Angular DOM 更新机制

视图

若是你读过我以前写的文章,尤为是 译 Angular DOM 更新机制,你可能会明白 Angular 程序内部是一棵视图树,每个视图都是由视图工厂生成的,而且每一个视图包含具备特定功能的不一样视图节点。在刚刚提到的文章中(那篇文章对了解本文很重要嗷),我介绍过两个最简单的节点类型——元素节点定义和文本节点定义。元素节点定义是用来建立全部 DOM 元素节点,而文本节点定义是用来建立全部 DOM 文本节点web

因此若是你写了以下的一个模板:api

<div><h1>Hello {{name}}</h1></div>

Angular Compiler 将会编译这个模板,并生成两个元素节点,即 divh1 DOM 元素,和一个文本节点,即 Hello {{name}} DOM 文本。这些都是很重要的节点,由于没有它们,你在屏幕上看不到任何东西。可是组件合成模式告诉咱们能够嵌套组件,因此必然另外一种视图节点来嵌入组件。为了搞清楚这些特殊节点是什么,首先须要了解组件是由什么组成的。本质上,组件本质上是具备特定行为的 DOM 元素,而这些行为是在组件类里实现的。首先看下 DOM 元素吧。数组

自定义 DOM 元素

你可能知道在 html 里能够建立一个新的 HTML 标签,好比,若是不使用框架,你能够直接在 html 里插入一个新的标签:浏览器

<a-comp></a-comp>

而后查询这个 DOM 节点并检查类型,你会发现它是个彻底合法的 DOM 元素(注:你能够在一个 html 文件里试试这部分代码,甚至能够写上 <a-comp>A Component</a-comp>,结果是能够运行的,缘由见下文):

const element = document.querySelector('a-comp');
element.nodeType === Node.ELEMENT_NODE; // true

浏览器会使用 HTMLUnknownElement 接口来建立 a-comp 元素,这个接口又继承 HTMLElement 接口,可是它不须要实现任何属性或方法。你可使用 CSS 来装饰它,也能够给它添加事件监听器来监听一些广泛事件,好比 click 事件。因此正如我说的,a-comp 是一个彻底合法的 DOM 元素。

而后,你能够把它转变成 自定义 DOM 元素 来加强这个元素,你须要为它单首创建一个类并使用 JS API 来注册这个类:

class AComponent extends HTMLElement {...}
window.customElements.define('a-comp', AComponent);

这是否是和你一直在作的事情有些相似呀。

没错,这和你在 Angular 中定义一个组件很是相似,实际上,Angular 框架严格遵循 Web 组件标准可是为咱们简化了不少事情,因此咱们没必要本身建立 shadow root 并挂载到宿主元素(注:关于 shadow root 的概念网上资料不少,其实在 Chrome Dev Tools 里,点击右上角 settings,而后点击 Preferences -> Elements,打开 Show user agent shadow root 后,这样你就能够在 Elements 面板里看到不少 DOM 元素下的 shadow root)。然而,咱们在 Angular 中建立的组件并无注册为自定义元素,它会被 Angular 以特定方式去处理。若是你对没有框架时如何建立组件很好奇,你能够查看 Custom Elements v1: Reusable Web Components

如今已经知道,咱们能够建立任何一个 HTML 标签并在模板里使用它。因此,若是咱们在 Angular 的组件模板里使用这个标签,框架将会给这个标签建立元素定义(注:这是由 Angular Compiler 编译生成的):

function View_AppComponent_0(_l) {
    return jit_viewDef2(0, [
        jit_elementDef3(0, null, null, 1, 'a-comp', [], ...)
    ])
}

然而,你得须要在 module 或组件装饰器属性里添加 schemas: [CUSTOM_ELEMENTS_SCHEMA],来告诉 Angular 你在使用自定义元素,不然 Angular Compiler 会抛出错误(注:因此若是须要使用某个组件,你不得不在 module.declarationsmodule.entryComponentscomponent.entryComponents 去注册这个组件):

'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...

因此,咱们已经有了 DOM 元素可是尚未附着在元素上的类呢,那 Angular 里除了组件外还有其余特殊类没?固然有——指令。让咱们看看指令有些啥。

指令定义

你可能知道每个指令都有一个选择器,用来挂载到特定的 DOM 元素上。大多数指令使用属性选择器(attribute selectors),可是有一些也选择元素选择器(element selectors)。实际上,Angular 表单指令就是使用 元素选择器 form 来把特定行为附着在 html form元素上。

因此,让咱们建立一个空指令类,并把它附着在自定义元素上,再看看视图定义是什么样的:

@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);
}

如今 Angular Compiler 在视图定义函数的第二个参数数组里,添加了新生成的指令定义 jit_directiveDef4 节点,并放在元素定义节点 jit_elementDef3 后面。同时设置元素定义的 childCount 为 1,由于附着在元素上的全部指令都会被看作该元素的子元素。

指令定义是个很简单的节点定义,它是由 directiveDef 函数生成的,该函数参数列表以下(注:如今 Angular v5.x 版本略有不一样):

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 建立指令对象时,它会实例化一个指令类,并存储在视图节点的 provider data 属性里。

因此咱们看到组件其实仅仅是一个元素定义加上一个指令定义,但仅仅如此么?你可能知道 Angular 老是没那么简单啊!

组件展现

从上文知道,咱们能够经过建立一个自定义元素和附着在该元素上的指令,来模拟建立出一个组件。让咱们定义一个真实的组件,并把由该组件编译生成的视图工厂类,与咱们上面实验性的视图工厂类作个比较:

@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 使用两种视图节点来表示组件——元素节点定义和指令节点定义。可是当使用一个真实的组件时,就会发现这两个节点定义的参数列表仍是有些不一样的。让咱们看看有哪些不一样吧。

节点类型

节点类型(NodeFlags)是全部节点定义函数的第一个参数(注:最新 Angular v5.* 中参数列表有点点不同,如 directiveDef 中第二个参数才是 NodeFlags)。它其实是 NodeFlags 位掩码(注:查看源码,是用二进制表示的),包含一系列特定的节点信息,大部分在 变动检测循环 时被框架使用。而且不一样节点类型采用不一样数字:16384 表示简单指令节点类型(注:仅仅是指令,可看 TypeDirective);49152 表示组件指令节点类型(注:组件加指令,即 TypeDirective + Component)。为了更好理解这些标志位是如何被编译器设置的,让咱们先转换为二进制:

16384 =  100000000000000 // 15th bit set
49152 = 1100000000000000 // 15th and 16th bit set

若是你很好奇这些转换是怎么作的,能够查看我写的文章 The simple math behind decimal-binary conversion algorithms 。因此,对于简单指令 Angular 编译器会设置 15-th 位为 1:

TypeDirective = 1 << 14

而对于组件节点会设置 15-th16-th 位为 1:

TypeDirective = 1 << 14
Component = 1 << 15

如今明白为什么这些数字不一样了。对于指令来讲,生成的节点被标记为 TypeDirective 节点;对于组件指令来讲,生成的节点除了被标记为 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'])

Angular 是一个视图树,因此父视图须要有个对子视图的引用,子视图会被存储在元素节点内。本例中,a-comp 的视图存储在为 <a-comp></a-comp> 生成的宿主元素节点内(注:意思就是 AComponent 视图存储在该组件宿主元素的元素定义内,就是存在 componentView 属性里。也能够查看 _Host.ngfactory.js 文件,该文件表示宿主元素 <a-comp></a-comp> 的工厂,里面存储 AComponent 视图对象)。jit_View_AComponent_04 参数是一个 代理类 的引用,这个代理类将会解析 工厂函数 建立一个 视图定义。每个视图定义仅仅建立一次,而后存储在 DEFINITION_CACHE,而后这个视图定义函数被 Angular 用来 建立视图对象

注:这段因为涉及大量的源码函数,会比较晦涩。做者讲的是建立视图的具体过程,细致到不少函数的调用。总之,只须要记住一点就行:视图解析器经过解析视图工厂(ViewDefinitionFactory)获得视图(ViewDefinition)。细节暂不用管。

拿到了视图,又该如何画出来呢?看下文。

组件渲染器类型

Angular 根据组件装饰器中定义的 ViewEncapsulation 模式来决定使用哪一种 DOM 渲染器:

以上组件渲染器是经过 DomRendererFactory2 来建立的。componentRendererType 参数是在元素定义里被传入的,本例便是 jit__object_Object_5(注:上面代码里有这个对象,是 jit_elementDef3() 的最后一个参数),该参数是渲染器的一个基本描述符,用来决定使用哪个渲染器渲染组件。其中,最重要的是视图封装模式和所用于组件的样式(注:componentRendererType 参数的结构是 RendererType2):

{
  styles:[["h1[_ngcontent-%COMP%] {color: green}"]], 
  encapsulation:0
}

若是你为组件定义了样式,编译器会自动设置组件的封装模式为 ViewEncapsulation.Emulated,或者你能够在组件装饰器里显式设置 encapsulation 属性。若是没有设置任何样式,而且也没有显式设置 encapsulation 属性,那描述符会被设置为 ViewEncapsulation.Emulated,并被 忽略生效,使用这种描述符的组件会使用父组件的组件渲染器。

子指令

如今,最后一个问题是,若是咱们像下面这样,把一个指令做用在组件模板上,会生成什么:

<a-comp adir></a-comp>

咱们已经知道当为 AComponent 生成工厂函数时,编译器会为 a-comp 元素建立元素定义,会为 AComponent 类建立指令定义。可是因为编译器会为每个指令生成指令定义节点,因此上面模板的工厂函数像这样(注:Angular v5.* 版本是会为 <a-comp></a-comp> 元素单独生成一个 *_Host.ngfactory.js 文件,表示宿主视图,多出来的 jit_directiveDef6(16384, null, 0, jit_ADirective8, [], ...) 是在这个文件代码里。能够 ng cli 新建项目查看 Sources Tab -> ng://。但做者表达的意思仍是同样的。):

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。

以上就是所有了!

注:全文主要讲的是组件(视图)在 Angular 内部是如何用指令节点和元素节点定义的。
相关文章
相关标签/搜索