[译] 为何 Angular 内部没有发现组件
原文链接: Here is why you will not find components inside Angular
Component is just a directive with a template? Or is it? 从我开始使用 Angular 开始,就被组件和指令间区别的问题所困惑,尤其对那些从 Angular.js 世界来的人,因为 Angular.js 里只有指令,尽管我们也经常把它当做组件来使用。如果你在网上搜这个问题解释,很多都会这么解释(注:为清晰理解,不翻译): Components are just directives with a content defined in a template… 这些说法貌似都对,我在查看由 Angular 编译器编译组件生成的视图工厂源码里,的确没发现组件定义,你如果查看也只会发现 指令。 注:使用 Angular-CLI ng new 一个新项目,执行 ng serve 运行程序后,就可在 Chrome Dev Tools 的 Source Tab 的 ng:// 域下查看到编译组件后生成的 **.ngfactory.js 文件,该文件代码即上面说的视图工厂源码。 但是我在网上没有找到 原因解释,因为想要知道原因就必须对 Angular 内部工作原理比较熟悉,如果上面的问题也困让了你很长一段时间,那本文正适合你。让我们一起探索其中的奥秘并做好准备吧。 本质上,本文主要解释 Angular 内部是如何定义组件和指令的,并引入新的视图节点定义——指令定义。 注:视图节点还包括元素节点和文本节点,有兴趣可查看 译 Angular DOM 更新机制 。 视图如果你读过我之前写的文章,尤其是 译 Angular DOM 更新机制,你可能会明白 Angular 程序内部是一棵视图树,每一个视图都是由视图工厂生成的,并且每个视图包含具有特定功能的不同视图节点。在刚刚提到的文章中(那篇文章对了解本文很重要嗷),我介绍过两个最简单的节点类型——元素节点定义和文本节点定义。元素节点定义是用来创建所有 DOM 元素节点,而文本节点定义是用来创建所有 DOM 文本节点 。 所以如果你写了如下的一个模板: <div><h1>Hello {{name}}</h1></div> Angular Compiler 将会编译这个模板,并生成两个元素节点,即 自定义 DOM 元素你可能知道在 html 里可以创建一个新的 HTML 标签,比如,如果不使用框架,你可以直接在 html 里插入一个新的标签: <a-comp></a-comp> 然后查询这个 DOM 节点并检查类型,你会发现它是个完全合法的 DOM 元素(注:你可以在一个 html 文件里试试这部分代码,甚至可以写上 const element = document.querySelector('a-comp'); element.nodeType === Node.ELEMENT_NODE; // true 浏览器会使用 HTMLUnknownElement 接口来创建 然后,你可以把它转变成 自定义 DOM 元素 来增强这个元素,你需要为它单独创建一个类并使用 JS API 来注册这个类: class AComponent extends HTMLElement {...} window.customElements.define('a-comp',AComponent); 这是不是和你一直在做的事情有些类似呀。 没错,这和你在 Angular 中定义一个组件非常类似,实际上,Angular 框架严格遵循 Web 组件标准但是为我们简化了很多事情,所以我们不必自己创建 现在已经知道,我们可以创建任何一个 HTML 标签并在模板里使用它。所以,如果我们在 Angular 的组件模板里使用这个标签,框架将会给这个标签创建元素定义(注:这是由 Angular Compiler 编译生成的): function View_AppComponent_0(_l) { return jit_viewDef2(0,[ jit_elementDef3(0,null,1,'a-comp',[],...) ]) } 然而,你得需要在 '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 来把特定行为附着在 所以,让我们创建一个空指令类,并把它附着在自定义元素上,再看看视图定义是什么样的: @Directive({selector: 'a-comp'}) export class ADirective {} 然后核查下生成的视图工厂: function View_AppComponent_0(_l) { return jit_viewDef2(0,...),jit_directiveDef4(16384,jit_ADirective5,...) ],null); } 现在 Angular Compiler 在视图定义函数的第二个参数数组里,添加了新生成的指令定义 指令定义是个很简单的节点定义,它是由 directiveDef 函数生成的,该函数参数列表如下(注:现在 Angular v5.x 版本略有不同):
本文我们只对 ctor 参数感兴趣,它仅仅是我们定义的 所以我们看到组件其实仅仅是一个元素定义加上一个指令定义,但仅仅如此么?你可能知道 Angular 总是没那么简单啊! 组件展示从上文知道,我们可以通过创建一个自定义元素和附着在该元素上的指令,来模拟创建出一个组件。让我们定义一个真实的组件,并把由该组件编译生成的视图工厂类,与我们上面实验性的视图工厂类做个比较: @Component({ selector: 'a-comp',template: '<span>I am A component</span>' }) export class AComponent {} 做好准备了么?下面是生成的视图工厂类: function View_AppComponent_0() { return jit_viewDef2(0,... jit_View_AComponent_04,jit__object_Object_5),jit_directiveDef6(49152,jit_AComponent7,...) 好的,现在我们仅仅验证了上文所说的。本示例中, Angular 使用两种视图节点来表示组件——元素节点定义和指令节点定义。但是当使用一个真实的组件时,就会发现这两个节点定义的参数列表还是有些不同的。让我们看看有哪些不同吧。 节点类型节点类型(NodeFlags)是所有节点定义函数的第一个参数(注:最新 Angular v5.* 中参数列表有点点不一样,如 directiveDef 中第二个参数才是 NodeFlags)。它实际上是 NodeFlags 位掩码(注:查看源码,是用二进制表示的),包含一系列特定的节点信息,大部分在 变更检测循环 时被框架使用。并且不同节点类型采用不同数字: 16384 = 100000000000000 // 15th bit set 49152 = 1100000000000000 // 15th and 16th bit set 如果你很好奇这些转换是怎么做的,可以查看我写的文章 The simple math behind decimal-binary conversion algorithms 。所以,对于简单指令 Angular 编译器会设置 TypeDirective = 1 << 14 而对于组件节点会设置 TypeDirective = 1 << 14 Component = 1 << 15 现在明白为何这些数字不同了。对于指令来说,生成的节点被标记为 视图定义解析器因为 <span>I am A component</span> 编译器会编译它,生成一个带有视图定义和视图节点的工厂函数: function View_AComponent_0(_l) { return jit_viewDef1(0,[ jit_elementDef2(0,'span',jit_textDef3(null,['I am A component']) Angular 是一个视图树,所以父视图需要有个对子视图的引用,子视图会被存储在元素节点内。本例中, 注:这段由于涉及大量的源码函数,会比较晦涩。作者讲的是创建视图的具体过程,细致到很多函数的调用。总之,只需要记住一点就行:视图解析器通过解析视图工厂(ViewDefinitionFactory)得到视图(ViewDefinition)。细节暂不用管。 组件渲染器类型Angular 根据组件装饰器中定义的 ViewEncapsulation 模式来决定使用哪种 DOM 渲染器:
以上组件渲染器是通过 DomRendererFactory2 来创建的。 { styles:[["h1[_ngcontent-%COMP%] {color: green}"]],encapsulation:0 } 如果你为组件定义了样式,编译器会自动设置组件的封装模式为 子指令现在,最后一个问题是,如果我们像下面这样,把一个指令作用在组件模板上,会生成什么: <a-comp adir></a-comp> 我们已经知道当为 function View_AppComponent_0() { return jit_viewDef2(0,2,... jit_View_AComponent_04,...) jit_directiveDef6(16384,...) 上面代码都是我们熟悉的,仅仅是多添加了一个指令定义,和子组件数量增加为 2。 以上就是全部了! 注:全文主要讲的是组件(视图)在 Angular 内部是如何用指令节点和元素节点定义的。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |