Giter VIP home page Giter VIP logo

blog's Introduction

Hi there 👋 Welcome to my GitHub!

┌───┐   ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┐
│Esc│   │ F1│ F2│ F3│ F4│ │ F5│ F6│ F7│ F8│ │ F9│F10│F11│F12│ │P/S│S L│P/B│  ┌┐    ┌┐    ┌┐
└───┘   └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┘  └┘    └┘    └┘
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───────┐ ┌───┬───┬───┐ ┌───┬───┬───┬───┐
│~ `│! 1│@ 2│# 3│$ 4│% 5│^ 6│& 7│* 8│( 9│) 0│_ -│+ =│ BacSp │ │Ins│Hom│PUp│ │N L│ / │ * │ - │
├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─────┤ ├───┼───┼───┤ ├───┼───┼───┼───┤
│ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{ [│} ]│ | \ │ │Del│End│PDn│ │ 7 │ 8 │ 9 │   │
├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤ └───┴───┴───┘ ├───┼───┼───┤ + │
│ Caps │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter  │               │ 4 │ 5 │ 6 │   │
├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────────┤     ┌───┐     ├───┼───┼───┼───┤
│ Shift  │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│  Shift   │     │ ↑ │     │ 1 │ 2 │ 3 │   │
├─────┬──┴─┬─┴──┬┴───┴───┴───┴───┴───┴──┬┴───┼───┴┬────┬────┤ ┌───┼───┼───┐ ├───┴───┼───┤ E││
│ Ctrl│    │Alt │         Space         │ Alt│ Fn │    │Ctrl│ │ ← │ ↓ │ → │ │   0   │ . │←─┘│
└─────┴────┴────┴───────────────────────┴────┴────┴────┴────┘ └───┴───┴───┘ └───────┴───┴───┘

blog's People

Contributors

stbui avatar

Watchers

 avatar  avatar  avatar

blog's Issues

Angular Elements 及其运作原理

现在,Angular Elements 这个项目已经在社区引起一定程度的讨论。这是显而易见的,因为 Angular Elements 提供了很多开箱即用的、十分强大的功能:

  • 通过使用原生的 HTML 语法来使用 Angular Elements —— 这意味着不再需要了解 Angular 的相关知识
  • 它是自启动的,并且一切都可以按预期那样运作
  • 它符合 Web Components 规范,这意味着它可以在任何地方使用
  • 虽然你没有使用 Angular 开发整个网站,但你仍然可以从 Angular Framework 这个庞大的体系中收益

@angular/elements这个包提供可将 Angular 组件转化为原生 Web Components 的功能,它基于浏览器的 Custom Elements API 实现。Angular Elements 提供一种更简洁、对开发者更友善、更快乐地开发动态组件的方式 —— 在幕后它基于同样的机制(指创建动态组件),但隐藏了许多样板代码。

关于如何通过 @angular/elements 创建一个 Custom Element,已经有大量的文章进行阐述,所以在这篇文章将深入一点,对它在 Angular 中的具体工作原理进行剖析。这也是我们开始研究 Angular Elements 的一系列文章的原因,我们将在其中详细解释 Angular 如何在 Angular Elements 的帮助下实现 Custom Elements API。

Custom Elements(自定义元素)

要了解更多关于 Custom Elements 的知识,可以通过 developers.google 中的这篇文章进行学习,文章详细介绍了与 Custom Elements API 相关的内容。

这里针对 Custom Elements,我们使用一句话来概括:

使用 Custom Elements,web 开发者可以创建一个新的 HTML 标签、增加已有的 HTML 标签以及继承其他开发者所开发的组件。

原生 Custom Elements

让我们来看看下面的例子,我们想要创建一个拥有 name 属性的 app-hello HTML 标签。可以通过 Custom Elements API 来完成这件事。在文章的后续章节,我们将演示如何使用 Angular 组件的 @input 装饰器与 这个 name 属性保持同步。但是现在,我们不需要使用 Angular Elements 或者 ShadowDom 或者使用任何关于 Angular 的东西来创建一个 Custom Element,我们仅使用原生的 Custom Components API。

首先,这是我们的 HTML 标记:

<hello-elem name="Custom Elements"></hello-elem>

要实现一个 Custom Element,我们需要分别实现如下在标准中定义的 hooks:

callback summary
constructor 如果需要的话,可在其中初始化 state 或者 shadowRoot,在这篇文章中,我们不需要
connectedCallback 在元素被添加到 DOM 中时会被调用,我们将在这个 hook 中初始化我们的 DOM 结构和事件监听器
disconnectedCallback 在元素从 DOM 中被移除时被调用,我们将在这个 hook 中清除我们的 DOM 结构和事件监听器
attributeChangedCallback 在元素属性变化时被调用,我们将在这个 hook 中更新我们内部的 dom 元素或者基于属性改变后的状态

如下是我们关于 Hello Custom Element 的实现代码:

class AppHello extends HTMLElement {
  constructor() {
    super();
  }
  // 这里定义了那些需要被观察的属性,当这些属性改变时,attributeChangedCallback 这个 hook 会被触发
  static get observedAttributes() {return ['name']; }

  // getter to do a attribute -> property reflection
  get name() {
    return this.getAttribute('name');
  }

  // setter to do a property -> attribute reflection
  // 通过 setter 来完成类属性到元素属性的映射操作
  set name(val) {
    this.setAttribute('name', val);
  }

  connectedCallback() {
    this.div = document.createElement('div');
    this.text = document.createTextNode(this.name || '');
    this.div.appendChild(this.text);
    this.appendChild(this.div);
  }

  disconnectedCallback() {
    this.removeChild(this.div);
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    if (attrName === 'name' && this.text) {
      this.text.textContent = newVal;
    }
  }
}

customElements.define('hello-elem', AppHello);

这里是可运行实例的链接。这样我们就实现了第一版的 Custom Element,回顾一下,这个 app-hellp 标签包含一个文本节点,并且这个节点将会渲染通过 app-hello 标签 name 属性传递进来的任何内容,这一切仅仅基于原生 javascript。

将 Angular 组件导出为 Custom Element

既然我们已经了解了关于实现一个 HTML Custom Element 所涉及的内容,让我们来使用 Angular实现一个相同功能的组件,之后再使它成为一个可用的 Custom Element。

首先,让我们从一个简单的 Angular 组件开始:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-hello',
  template: `<div>{{name}}</div>`
})
export class HelloComponent  {
  @Input() name: string;
}

正如你所见,它和上面的例子在功能上一模一样。

现在,要将这个组件包装为一个 Custom Element,我们需要创建一个 wrapper class 并实现所有 Custom Elements 中定义的 hooks:

class HelloComponentClass extends HTMLElement {
  constructor() {
    super();
  }

  static get observedAttributes() {
  }

  connectedCallback() {
  }

  disconnectedCallback() {
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
  }
}

下一步,我们要做的是桥接 HelloComponent 和 HelloComponentClass。它们之间的桥会将 Angular Component 和 Custom Element 连接起来,如图所示:

image

要完成这座桥,让我们来依次实现 Custom Elements API 中所要求的每个方法,并在这个方法中编写关于绑定 Angular 的代码:

callback summary angular part
constructor 初始化内部状态 进行一些准备工作
connectedCallback 初始化视图、事件监听器 加载 Angular 组件
disconnectedCallback 清除视图、事件监听器 注销 Angular 组件
attributeChangedCallback 处理属性变化 处理 @input 变化

1. constructor()

我们需要在 connectedCallback() 方法中初始化 HelloComponent,但是在这之前,我们需要在 constructor 方法中进行一些准备工作。

顺便,关于如何动态构造 Angular 组件可以通过阅读Dynamic Components in Angular这篇文章进行了解。它其中阐述的运作机制和我们这里使用的一模一样。

所以,要让我们的 Angular 动态组件能够正常工作(需要 componentFactory 能够被编译),我们需要将 HelloComponent 添加到 NgModule 的 entryComponents 属性(它是一个列表)中去:

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [HelloComponent],
  entryComponents: [HelloComponent]
})
export class CustomElementsModule {
  ngDoBootstrap() {}
}

基本上,调用 prepare() 方法会完成两件事:

  • 它会基于组件的定义初始化一个 factoryComponent 工厂方法
  • 它会基于 Angular 组件的 inputs 初始化 observedAttributes,以便我们在 attributeChangedCallback() 中完成我们需要做的事
class AngularCustomElementBridge {
  prepare(injector, component) {
    this.componentFactory = injector.get(ComponentFactoryResolver).resolveComponentFactory(component);

    // 我们使用 templateName 来处理 @Input('aliasName') 这种情形
    this.observedAttributes = componentFactory.inputs.map(input => input.templateName); 
  }
}

2. connectedCallback()

在这个回调函数中,我们将看到:

  • 初始化我们的 Angular 组件(就如创建动态组件那样)
  • 设置组件的初始 input 值
  • 在渲染组件时,触发脏检查机制
  • 最后,将 HostView 增加到 ApplicationRef

如下是实战代码:

class AngularCustomElementBridge {
  initComponent(element: HTMLElement) {
    // 首先我们需要 componentInjector 来初始化组件
    // 这里的 injector 是 Custom Element 外部的注入器实例,调用者可以在这个实例中注册
    // 他们自己的 providers
    const componentInjector = Injector.create([], this.injector);
  
    this.componentRef = this.componentFactory.create(componentInjector, null, element);

    // 然后我们要检查是否需要初始化组件的 input 的值
    // 在本例中,在 Angular Element 被加载之前,user 可能已经设置了元素的属性
    // 这些值被保存在 initialInputValues 这个 map 结构中
    this.componentFactory.inputs.forEach(prop => this.componentRef.instance[prop.propName] = this.initialInputValues[prop.propName]);

    // 之后我们会触发脏检查,这样组件在事件循环的下一个周期会被渲染
    this.changeDetectorRef.detectChanges();
    this.applicationRef = this.injector.get(ApplicationRef);

    // 最后,我们使用 attachView 方法将组件的 HostView 添加到 applicationRef 中
    this.applicationRef.attachView(this.componentRef.hostView);
  }
}

3. disconnectedCallback()

这个十分容易,我们仅需要在其中注销 componentRef 即可:

class AngularCustomElementBridge {
  destroy() {
    this.componentRef.destroy();
  }
}

4. attributeChangedCallback()

当元素属性发生改变时,我们需要相应地更新 Angular 组件并触发脏检查:

class AngularCustomElementBridge {
  setInputValue(propName, value) {
    if (!this.componentRef) {
      this.initialInputValues[propName] = value;
      return;
    }
    if (this.componentRef[propName] === value) {
      return;
    }
    this.componentRef[propName] = value;
    this.changeDetectorRef.detectChanges();
  }
}

5. Finally, we register the Custom Element

customElements.define('hello-elem', HelloComponentClass);

这是一个可运行的例子链接

总结

这就是根本**。通过在 Angular 中使用动态组件,我们简单实现了 Angular Elements 所提供的基础功能,重要的是,没有使用 @angular/element 这个库。
当然,不要误解 —— Angular Elements 的功能十分强大。文章中所涉及的所有实现逻辑在 Angular Elements 都已被抽象化,使用这个库可以使我们的代码更优雅,可读性和维护性也更好,同时也更易于扩展。
以下是关于 Angular Elements 中一些模块的概要以及它们与这篇文章的关联性:

  • create-custom-element.ts:这个模块实现了我们在这篇文章中讨论的关于 Custom Element 的几个回调函数,同时它还会初始化一个 NgElementStrategy 策略类,这个类会作为连接 Angular Component 和 Custom Elements 的桥梁。当前,我们仅有一个策略 —— component-factory-strategy.ts —— 它的运作机制与本文例子中演示的大同小异。在将来,我们可能会有其他策略,并且我们还可以实现自定义策略。
  • component-factory-strategy.ts:这个模块使用一个 component 工厂函数来创建和销毁组件引用。同时它还会在 input 改变时触发脏检查。这个运作过程在上文的例子中也有被提及。

下次我们将阐述 Angular Elements 通过 Custom Events 输出事件。

原文:https://blog.angularindepth.com/angular-elements-how-does-this-magic-work-under-the-hood-3684a0b2be95

rancheros 安装

实现环境

  • VMware® Workstation 12 Pro 12.5.6 build-5528349
  • rancheros-1.0.4-docker-17.03.1-ce-Linux-4.9.40

修改虚拟机系统

修改root密码

sudo bash
passwd rancher

ssh连接

ssh [email protected]

添加 cloud-config 文件

vi cloud-config.yml

添加内容:


hostname: rancheros-stbui

ssh_authorized_keys:
  - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXi7gAB95U3Wjrh9OzToqIzKTFISipLJylAHCzpm4KlV25dDheGSL+RuonDTK0uSDbqMiw0Kg85pdkcDfdmZ+bX8LCRbwazSwM9C/xHTgoa0DRhy/VDb7SmZQ10aSwOTQPelyL3rN71e3oXdFZ35KVJTWXtKoLNSU0A856jPCVR4fiIZUK7iMDGl6XKJKiG63lYBrswzTnfMiC9EDuU1n3gHJk1jToO/Q46sif2pgc0ZQmeUmkngM/vvLyYYChXd6z7JTm9o3vZAmCuyHlAma1m3qDElQF4EMYNgvxx45xVzx5UitQQcq0ujYXhb7KzsGmLWsdm29onqwT0T7Jd2ed [email protected]
sudo ros install -c cloud-config.yml -d /dev/sda

ssh 秘钥生成

ssh-keygen -t rsa
cat ~/.ssh/id_rsa.pud

Angular中的AoT编译

最近我给angular-seed增加了对Ahead-of-Time(AoT)编译的支持,这引来了不少关于这个新特性的问题。我们从下面这些话题开始来回答这些问题:

  • 为什么Angular需要编译?
  • 什么东西会被编译?
  • 他们是如何被编译的?
  • 编译发生在什么时候?JiT vs AoT
  • 我们从AoT中获得了什么?
  • AoT编译是如何工作的?
  • 我们使用AoT和JiT的代价是什么?

为什么Angular需要编译?

这个问题的简短回答是:编译可以让Angular应用达到更高层度的运行效率,我所说的效率,主要是指的性能提升,但也包括电池节能和节省流量。

AngularJs1.x 有一个实现渲染和变化检测的很动态的方式,比如AngularJs1.x的编译器非常通用,它被设计为任何模板实现一系列的动态计算,虽然它在通常情况下运行的很好,但是JS虚拟机的动态特性让一些低层次的计算优化变得很困难。由于js虚拟机无法理解那些我们作为脏检查的上下文对象(术语为scope)的形态,虚拟机的内联缓存常常不精确,这导致了运行效率的下降。

译者:scope是AngularJs1.x中的一个重要对象,他是AngularJs1.x用于计算模板的上下文。

Angular2+采用了一个不同的方式。在给每个组件做渲染和变化检测的时候,它不再使用同一套逻辑,框架在运行时或者编译时会生成对js虚拟机友好的代码。这些友好的代码可以让js虚拟机在属性访问的缓存,执行变化检查,进行渲染的逻辑执行的快的多。

举个例子,看看下面的代码:

// ...
Scope.prototype.$digest = function () {
  'use strict';
  var dirty, watcher, current, i;
  do {
    dirty = false;
    for (i = 0; i < this.$$watchers.length; i += 1) {
      watcher = this.$$watchers[i];
      current = this.$eval(watcher.exp);
      if (!Utils.equals(watcher.last, current)) {
        watcher.last = Utils.clone(current);
        dirty = true;
        watcher.fn(current);
      }
    }
  } while (dirty);
  for (i = 0; i < this.$$children.length; i += 1) {
    this.$$children[i].$digest();
  }
};
// ...

这个代码片段来自《轻量级angularJs1.x实现》一文。这些代码实现了对scope树做深度优先搜索,目的是为了寻找绑定数据的变化,这个方法对任何指令都生效。这些代码显然比下面这些直接指定检查的代码慢:

// ...
var currVal_6 = this.context.newName;
if (import4.checkBinding(throwOnChange, this._expr_6, currVal_6)) {
    this._NgModel_5_5.model = currVal_6;
    if ((changes === null)) {
        (changes = {});
    }
    changes['model'] = new import7.SimpleChange(this._expr_6, currVal_6);
    this._expr_6 = currVal_6;
}
this.detectContentChildrenChanges(throwOnChange);
// ...

译者:
《轻量级angularJs1.x实现》的地址是 <https://github.com/mgechev/light-angularjs/blob/master/lib/Scope.js#L61-L79
这里一下子提及了angularJs1.x的好几个概念,包括scope,数据绑定,指令。不熟悉angularJs1.x的同学理解起来费劲,想弄懂的话,自行搜索吧。个人认为可以无视,毕竟这个文章的重点不是在这里。你就认为Angular2+的处理方式比angularJs1.x牛逼很多就好了,哈哈。

上面代码包含了一个来自angular-seed的某个编译后的组件的代码,这些代码是由编译器生成的,包含了一个 detectChangesInternal 方法的实现。Angular框架通过直接属性访问的方式读取了数据绑定中的某些值,并且采用了最高效的方式与新的值做比较。一旦Angular框架发现这些值发生了变化,它就立即更新只受这些数据波及的DOM元素。

在回答了“为什么Angular需要编译”这个问题的同时,我们同时也回答了“什么东西会被编译”这个问题。我们希望把组件的模板编译成一个JS类,这些类包含了在绑定的数据中检测变化和渲染UI的逻辑。通过这个方式,我们和潜在的平台解耦了。换句话说,通过对渲染器采取了不同的实现,我们在不对代码做任何的修改的前提下,就可以对同一个以AoT方式编译的组件做不同的渲染。比如,上述代码中的组件还可以直接用在NativeScript中,这是由于这些不同的渲染器都能够理解编译后的组件。

编译发生在什么时候?JiT 还是 AoT

Angular编译器最cool的一点是它可以在页面运行时(例如在用户的浏览器内)启动,也可以作为构建的一个步骤在页面的编译时启动。这主要得益于Angular的可移植性:我们可以在任何的平台的JS虚拟机上运行Angular,所以我们完全可以在浏览器和NodeJs中运行它。

JiT编译模式的流程

一个典型的非AoT应用的开发流程大概是:

  • 使用TypeScript开发Angular应用
  • 使用tsc来编译这个应用的ts代码
  • 打包
  • 压缩
  • 部署

一旦把app部署好了,并且用户在浏览器中打开了这个app,下面这些事情会逐一进行:

  • 浏览器下载js代码
  • Angular启动
  • Angular在浏览器中开始JiT编译的过程,例如生成app中各个组件的js代码
  • 应用页面得以渲染

AoT编译模式的流程

相对的,使用AoT模式的应用的开发流程是:

  • 使用TypeScript开发Angular应用
  • 使用ngc来编译应用
    • 使用Angular编译器对模板进行编译,生成TypeScript代码
    • ypesScript代码编译为JavaScript代码
  • 打包
  • 压缩
  • 部署

虽然前面的过程稍稍复杂,但是用户这一侧的事情就变简单了:

  • 下载所以代码
  • Angular启动
  • 应用页面得以渲染

如你所见,第三步被省略掉了,这意味着页面打开更快,用户体验也更好。类似Angular-cli和Angular-seed这样的工具可以让整个编译过程变的非常的自动化。

概括起来,Angular中的Jit和AoT的主要区别是:

  • 编译过程发生的时机
  • JiT生成的是JS代码,而AoT生成的是TS代码。这主要是因为JiT是在浏览器中进行的,它完全没必要生成TS代码,而是直接生产了JS代码。

你可以在我的Github账号中找到一个最小的AoT编译demo,链接在这里 https://github.com/mgechev/angular2-ngc-rollup-build

深入AoT编译

这个小节回答了这些问题:

  • AoT编译过程产生了什么文件?
  • 这些产生的文件的上下文是什么?
  • 如何开发出AoT友好又有良好封装的代码?

@angular/compiler的代码一行一行的解释没太大意义,因此我们仅仅来快速过一下编译的过程。如果你对编译器的词法分析过程,解析和生成代码过程等感兴趣,你可以读一读Tobias Bosch的《Angular2编译器》一文,或者它的胶片。

译者:
《Angular2编译器》一文链接 https://www.youtube.com/watch?v=kW9cJsvcsGo
它的胶片链接 https://speakerdeck.com/mgechev/angular-toolset-support?slide=69

Angular模板编译器收到一个组件和它的上下文(可以这认为是组件在组件树上的位置)作为输入,并产生了如下文件:

  • *.ngfactory.ts 我们在说明组件上下文的小节会仔细看看这些文件
  • *.css.shim.ts 样式作用范围被隔离后的css文件,根据组件所设置的 ViewEncapsulation 模式不同而会有不同
  • *.metadata.json 当前组件/模块的装饰器元数据信息,这些数据可以被想象成以json格式传递给 @Componen @NgModule 装饰器的信息。

* 是一个文件名占位符,例如对于hero.component.ts这样的组件,编译器生成的文件是hero.component.ngfactory.ts, hero.component.css.shim.tshero.component.metadata.json*.css.shim.ts和我们讨论的主题关系不大,因此不会对它详细描述。如果你希望多了解 *.metadata.json 文件,你可以看看“AoT和第三方模块”小节。

*.ngfactory.ts 的内部结构

它包含了如下的定义:

  • _View_{COMPONENT}_Host{COUNTER} 我们称之为internal host component
  • _View_{COMPONENT}{COUNTER} 我们称之为 internal component

以及下面两个函数

  • viewFactory_{COMPONENT}_Host{COUNTER}
  • viewFactory_{COMPONENT}{COUNTER}

其中的 {COMPONENT} 是组件的控制器名字,而 {COUNTER} 是一个无符号整数。他们都继承了 AppView,并且实现了下面的方法:

  • createInternal 组件的渲染器
  • destroyInternal 执行事件监听器等的清理
  • detectChangesInternal 以内联缓存优化后的逻辑执行变化检测

上述这些工厂函数只在生成的AppView实例中才存在。

我前面说过,detectChangesInternal中的代码是JS虚拟机友好的。

<div>{{newName}}</div>
<input type="text" [(ngModel)]="newName">

我们来看看编译后这个模板的代码,detectChangesInternal方法的代码看起来像是这样的:

// ...
var currVal_6 = this.context.newName;
if (import4.checkBinding(throwOnChange, this._expr_6, currVal_6)) {
    this._NgModel_5_5.model = currVal_6;
    if ((changes === null)) {
        (changes = {});
    }
    changes['model'] = new import7.SimpleChange(this._expr_6, currVal_6);
    this._expr_6 = currVal_6;
}
this.detectContentChildrenChanges(throwOnChange);
// ...

假设currVal_6的值是3,this_expr_6的值是1,我们来跟踪看看这个方法的执行。对于这样的一个调用 import4.checkBinding(1, 3),在生产环境下,checkBinding 执行的是下面的检查:

1 === 3 || typeof 1 === 'number' && typeof 3 === 'number' && isNaN(1) && isNaN(3);

上述表达式返回false,因此我们将把变化保持下来,以及直接更新 NgModel 的属性 model 的值,在这之后,detectContentChildrenChanges 方法会被调用,它将为整个模板内容的子级调用 detectChangesInternal。一旦 NgModel 指令发现了 model 属性发生了变化,它就会(几乎)直接调用渲染器来更新对应的DOM元素。

目前为止,我们还没有碰到任何特殊的,或者特别复杂的逻辑。

context 属性

也许你已经注意到了在internal component内部访问了 this.context 属性。

译者:internal component 指的前一小节的 View{COMPONENT}{COUNTER} 函数

internal component中的 context 是这个组件的控制器的实例,例如这样的一个组件:

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  hero: Hero;
}

this.context 就是 new HeroComponent(),这意味着如果在 detectChangesInternal 中我们需要访问 this.context.name 的话,就带来了一个问题:如果我们使用AoT模式编译组件的模板,由于这个模式会生成TypeScript代码,因此我们要确保在组件的模板中只访问 this.context 中的public成员。这是为何?由于TypeScript的类属性有访问控制,强制类外部只能访问类(及其父类)中的public成员,因此在internal component内部我们无法访问 this.context 的任何私有成员。因此,下面这个组件:

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  private hero: Hero;
}

以及这个组件

class Hero {
  private name: string;
}

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  hero: Hero;
}

在生成出来的 *.ngfactory.ts 中,都会抛出编译错误。第一个组件代码,internal component无法访问到在 HeroComponent 类中被声明为 private 的 hero 属性。第二个组件代码中,internal component无法访问到 hero.name 属性,因为它在 Hero 类中被声明为private。

AoT与封装

好吧,我们只能在组件模板中绑定public属性,以及调用public方法。但是,如何确保组件的封装性?在开始的时候,这可能不是一个大问题,但是想象一下下面这个场景:

// component.ts
@Component({
  selector: 'third-party',
  template: `
    {{ _initials }}
  `
})
class ThirdPartyComponent {
  private _initials: string;
  private _name: string;

  @Input()
  set name(name: string) {
    if (name) {
      this._initials = name.split(' ').map(n => n[0]).join('. ') + '.';
      this._name = name;
    }
  }
}

这个组件有一个属性 name,它只能写入而无法读取。在 name 属性的 setter 方法中,计算了 _initials 属性的值。

我们可以用类似下面的方式使用这个组件:

@Component({
  template: '<third-party [name]="name"></third-party>'
  // ...
})
// ...

在JiT编译模式下,一切正常,因为JiT模式只生成JavaScript代码。每次 name 属性的值发生变化,_initials 就会被重新计算。但是,这个组件却不是AoT友好的,必须改为

// component.ts
@Component({
  selector: 'third-party',
  template: `
    {{ initials }}
  `
})
class ThirdPartyComponent {
  initials: string;
  private _name: string;

  @Input()
  set name(name: string) {...}
}

codelyzer 这个工具可以确保你在模板中每次都能访问到public成员。

这让组件的使用者可以这样做:

import {ThirdPartyComponent} from 'third-party-lib';

@Component({
  template: '<third-party [name]="name"></third-party>'
  // ...
})
class Consumer {
  @ViewChild(ThirdPartyComponent) cmp: ThirdPartyComponent;
  name = 'Foo Bar';

  ngAfterViewInit() {
    this.cmp.initials = 'M. D.';
  }
}

对public属性 initials 的直接修改导致了组件处于不一致性的状态:组件的 _name 的值是 Foo Bar,但是它的 initials 的值是 M. D.,而非 F. B.

在Angular的源码中,我们可以找到解决的办法,使用TypeScript的 /** @internal */ 注释声明,就能够达到既保证组件代码对AoT友好,又能够确保组件的封装良好的目的。

// component.ts
@Component({
  selector: 'third-party',
  template: `
    {{ initials }}
  `
})
class ThirdPartyComponent {
  /** @internal */
  initials: string;
  private _name: string;

  @Input()
  set name(name: string) {...}
}

initials 属性仍然是public的。我们在使用 tsc 编译这个组件时,设置 --stripInternal 和 --declarations 参数,initials 属性就会从组件的类型定义文件(即 .d.ts 文件)中被删掉。这样我们就可以做到在我们的类库内部使用它,但是我们的组件使用者无法使用它。

ngfactory.ts 概要

我们来对幕后所发生的一切做一些概要描述。拿我们前面的例子中的 HeroComponent 为例,Angular编译器会生成两个类:

  • _View_HeroComponent_Host1 这是 internal host component
  • _View_HeroComponent1 这是 internal component

_View_HeroComponent1 负责渲染这个组件的模板,以及进行变化检测。在执行变化检测时,它会对 this.context.hero.name之前保存的值和当前值做比较,一旦发现这两个值不一致,<h1/>元素就会被更新,这意味着我们必须保持 this.context.hero 和 hero.name 是public的。这一点可以通过 codelyzer 这个工具来辅助确保。

另外,_View_HeroComponent_Host1 则负责 和 _View_HeroComponent1 本身的渲染。

这个例子可以以下面这个图来总结:

image

AoT vs JiT 开发体验

这个小结,我们来讨论使用AoT开发和JiT开发的另一种体验。

可能使用JiT对开发体验的冲击最大的就是JiT模式为internal componentinternal host component生成的是JavaScript代码,这意味着组件的控制器中的属性都是public的,因此我们不会得到任何编译错误。

在JiT模式下,一旦我们启动了应用,根组件的根注入器和所有的指令就已经准备就绪了(他们被包含在 BrowserModule 和其他所有我们在根模块中引入的模块中了)。元数据信息会传递给编译器,用于对根组件的模板的编译。一旦编译器生成了JiT下的代码,编译器就拥有了用于生成各个子组件的所有元数据信息。由于编译器此时不仅知道了当前层级的组件有那些provider可用,还可以知道那些指令是可见的,因此它可以给所有的组件生成代码。

这一点让编译器在访问了模板中的一个元素时,知道该怎么工作。根据是否有选择器是 bar-baz 的指令/组件,<bar-baz></bar-baz> 这样的一个元素就有了两种不同的解释。编译器在创建了 <bar-baz></bar-baz> 这样的一个元素的同时,是否还同时初始化 bar-baz 对应的组件类的实例,则完全取决于当前阶段的编译过程的元数据信息。

这里有一个问题,在编译阶段,我们如何知道指令在整个组件树上是否可访问?得益于Angular框架的良好设计,我们通过静态代码分析就可以做到。Chuck Jazdzewski 和 Alex Eagle 在这个方向上做出了令人惊叹的成果,他们实现了 MetadataCollector 和相关的模块。MetadataCollector 所做的事情就是通过遍历组件树来获取每个组件和NgModule的元数据信息,这个过程中,很多牛逼的技术被用到,可惜这些技术超出了本文的范畴。

AoT与第三方模块

为了编译组件的模板,编译器需要组件的元数据信息,我们来假设我们的应用使用到了一些第三方组件,Angular的AoT编译器是如何获取这些已经被编译成JavaScript代码的组件的元数据信息的?这些组件库必须连带发布对应的 *.metadata.json文件,这样才能够对一个引用了它的页面进行AoT编译。

如果你想了解如何使用Angular编译器,例如如何编译你自定义库使得他们能够被用于以AoT编译的应用,那请访问这个链接 https://github.com/angular/mobile-toolkit/blob/master/app-shell/gulpfile.ts#L52-L54

我们从AoT中获得了什么?

你可能已经想到了,AoT给我们带来了性能的提升。以AoT方式开发的Angular应用的初次渲染性能要比JiT的高的多,这是由于JS虚拟机需要的计算量大大减少了。我们只在开发过冲中,将组件的模板编译成js代码,在此之后,用户不需要等待再次编译。

下面这个图中,可以看出JiT渲染方式在初始化过程中所消耗的时间:

image

下面这个图你可以看出AoT方式初始化在初始化过程中所消耗的时间:

image

Angular编译器不仅能够生产JavaScript,还能够生成TypeScript,这一点还带给我们要给非常棒的特性:在模板中进行类型检查。

由于应用的模板是纯JavaScript/TypeScript,我们可以精确的知道哪些东西在哪被用到了,这一点让我们可以对代码进行有效的摇树操作,它能够把所有的未使用过的指令/模块从我们生产环境中的应用代码包中给删除掉。这首要的一点是,我们的应用程序代码包中,再也无需包含 @angular/compiler 这个模块,因为我们在应用的运行时根本就用不到它。

有一点需要注意的是,一个中大型的应用代码包,在进行AoT编译过之后,可能会比使用JiT方式编译的代码包要大一些。这是因为 ngc 生成的对JS虚拟机友好的代码比基于HTML模板的代码要冗长一些,并且这些代码还包含了脏检查逻辑。如果你想降低你的应用代码的尺寸,你可以通过懒加载的方式来实现,Angular内建的路由已经支持这一点了。

在某些场合,JiT模式的编译根本就无法进行,这是由于JiT在浏览器中,不仅生成代码,它还使用 eval 来运行它们,浏览器的内容安全策略以及特定的环境不允许这些被生成的代码被动态的运行。

最后一个但不是唯一的:节能!在接受到的是编译后的代码时,用户的设备可以花更少的时间运行他们,这节约了电池的电力。节能的量有多少呢?下面是我做的一些有趣的计算的结果:

基于《Who Killed My Battery: Analyzing Mobile Browser Energy Consumption》这偏文章的结论,访问Wikipedia时,下载和解析jQuery的过程大约需要消耗4焦耳的能量。这个文章没有提及所使用的jQuery的确切版本,基于文章发表的日期,我估计版本号是1.8.x。Wikipedia采用了gzip对静态资源做压缩,这意味着jQuery1.8.3的尺寸约33k。而被最小化并且gzip压缩后的 @angular/compiler 包的尺寸在103k,这意味着对这些代码的下载和解析需要消耗12.5焦的能量(我们可以忽略JiT的运算还会增加能耗的事实,这是因为jQuery和@angular/compiler这两个场景,都是使用了要给单一的TCP链接,这从是最大的能耗所在)。

iPhone6s的电池容量是6.9Wh,即24840焦。基于AngularJs1.x官网的月访问量可以得知现在大约有一百万名Angular开发者,平均每位开发者构建了5个应用,每个应用每天约100名用户。5个app 1m 100用户 = 500m,在使用JiT编译这些应用,他们就需要下载 @angular/compiler 包,这将给地球带来 500m * 12.5J = 6250000000J焦(=1736.111111111千瓦时)的能量消耗。根据Google搜索结果,1千瓦时约等于12美分,这意味着我们每天需要消耗约 210 美元。注意到我们还没进一步对代码做摇树操作,这可能会让我们的应用程序代码降低至少一半!

结论

Angular的编译器利用了JS虚拟机的内联缓存机制,极大的提升了我们的应用程序的性能。首要的一点是我们把编译过程作为构建应用的一个环节,这不仅解决了非法eval的问题,还允许我们对代码做高效的摇树,降低了首次渲染的时间。

不在运行时编译是否让我们失去是什么吗?在一些非常极端的场景下,我们会按需生成组件的模板,这就需要我们价值一个未编译的组件,并在浏览器中执行编译的过程,在这样的场景下,我们就需要在我们的应用代码包中包含 @angular/compiler 模块。AoT编译的另一个潜在缺点是,它会造成中大型应用的代码包尺寸变大。由于生成的组件模板的JavaScript代码比组件模板本身的尺寸更大,这就可能造成最终代码包的尺寸更大一些。

总的来说,AoT编译是一个很好的技术,现在它已经被集成到了Angular-seed和angular-cli中,所以,你今天就可以去使用它了。

参考资料

原文:
https://blog.mgechev.com/2016/08/14/ahead-of-time-compilation-angular-offline-precompilation/

OS X 主机报错

docker: Error response from daemon: Mounts denied:
The path /var/lib/rancher
is not shared from OS X and is not known to Docker.
You can configure shared paths from Docker -> Preferences... -> File Sharing.
See https://docs.docker.com/docker-for-mac/osxfs/#namespaces for more info.
sudo docker run --rm --privileged -v /var/run/docker.sock:/var/run/docker.sock -v ${HOME}/docker/rancher/var/lib/rancher:/var/lib/rancher rancher/agent:v2.0-alpha4 http://192.168.0.101:8080/v3/scripts/F7F355A6643190547F84:1483142400000:NMmQ4gv1ug1jXayMZb46dFKiL4

别再对 Angular 表单的 ControlValueAccessor 感到迷惑

如果你正在做一个复杂项目,必然会需要自定义表单控件,这个控件主要需要实现 ControlValueAccessor 接口(译者注:该接口定义方法可参考 API 文档说明,也可参考 Angular 源码定义)。网上有大量文章描述如何实现这个接口,但很少说到它在 Angular 表单架构里扮演什么角色,如果你不仅仅想知道如何实现,还想知道为什么这样实现,那本文正合你的胃口。

首先我解释下为啥需要 ControlValueAccessor 接口以及它在 Angular 中是如何使用的。然后我将展示如何封装第三方组件作为 Angular 组件,以及如何使用输入输出机制实现组件间通信(译者注:Angular 组件间通信输入输出机制可参考 官网文档),最后将展示如何使用 ControlValueAccessor 来实现一种针对 Angular 表单新的数据通信机制。

FormControl 和 ControlValueAccessor

如果你之前使用过 Angular 表单,你可能会熟悉 FormControl ,Angular 官方文档将它描述为追踪单个表单控件值和有效性的实体对象。需要明白,不管你使用模板驱动还是响应式表单(译者注:即模型驱动),FormControl 都总会被创建。如果你使用响应式表单,你需要显式创建 FormControl 对象,并使用 formControlformControlName 指令来绑定原生控件;如果你使用模板驱动方法,FormControl 对象会被 NgModel 指令隐式创建(译者注:可查看 Angular 源码 这一行):

@Directive({
  selector: '[ngModel]...',
  ...
})
export class NgModel ... {
  _control = new FormControl();   <---------------- here

不管 formControl 是隐式还是显式创建,都必须和原生 DOM 表单控件如 input,textarea 进行交互,并且很有可能需要自定义一个表单控件作为 Angular 组件而不是使用原生表单控件,而通常自定义表单控件会封装一个使用纯 JS 写的控件如 jQuery UI's Slider 。本文我将使用原生表单控件术语来区分 Angular 特定的 formControl 和你在 html 使用的表单控件,但你需要知道任何一个自定义表单控件都可以和 formControl 指令进行交互,而不是原生表单控件如 input。

原生表单控件数量是有限的,但是自定义表单控件是无限的,所以 Angular 需要一种通用机制来桥接原生/自定义表单控件和 formControl 指令,而这正是 ControlValueAccessor 干的事情。这个对象桥接原生表单控件和 formControl 指令,并同步两者的值。官方文档是这么描述的(译者注:为清晰理解,该描述不翻译):

A ControlValueAccessor acts as a bridge between the Angular forms API and a native element in the DOM

任何一个组件或指令都可以通过实现 ControlValueAccessor 接口并注册为 NG_VALUE_ACCESSOR,从而转变成 ControlValueAccessor 类型的对象,稍后我们将一起看看如何做。另外,这个接口还定义两个重要方法——writeValue 和 registerOnChange (译者注:可查看 Angular 源码 这一行):

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  ...
}

formControl 指令使用 writeValue 方法设置原生表单控件的值(译者注:你可能会参考 L186L41);使用 registerOnChange 方法来注册由每次原生表单控件值更新时触发的回调函数(译者注:你可能会参考这三行,L186L43,以及 L85),你需要把更新的值传给这个回调函数,这样对应的 Angular 表单控件值也会更新(译者注:这一点可以参考 Angular 它自己写的 DefaultValueAccessor 的写法是如何把 input 控件每次更新值传给回调函数的,L52L89);使用 registerOnTouched 方法来注册用户和控件交互时触发的回调(译者注:你可能会参考 L95)。

下图是 Angular 表单控件 如何通过 ControlValueAccessor 来和原生表单控件交互的(译者注:formControl 和你写的或者 Angular 提供的 CustomControlValueAccessor 两个都是要绑定到 native DOM element 的指令,而 formControl 指令需要借助 CustomControlValueAccessor 指令/组件,来和 native DOM element 交换数据。):

image

再次强调,不管是使用响应式表单显式创建还是使用模板驱动表单隐式创建,ControlValueAccessor 都总是和 Angular 表单控件进行交互。
Angular 也为所有原生 DOM 表单元素创建了 Angular 表单控件(译者注:Angular 内置的 ControlValueAccessor):

+------------------------------------+----------------------+
|              Accessor              |     Form Element     |
+------------------------------------+----------------------+
| DefaultValueAccessor               | input, textarea      |
| CheckboxControlValueAccessor       | input[type=checkbox] |
| NumberValueAccessor                | input[type=number]   |
| RadioControlValueAccessor          | input[type=radio]    |
| RangeValueAccessor                 | input[type=range]    |
| SelectControlValueAccessor         | select               |
| SelectMultipleControlValueAccessor | select[multiple]     |

从上表中可看到,当 Angular 在组件模板中中遇到 input 或 textarea DOM 原生控件时,会使用DefaultValueAccessor 指令:

@Component({
  selector: 'my-app',
  template: `
      <input [formControl]="ctrl">
  `
})
export class AppComponent {
  ctrl = new FormControl(3);
}

所有表单指令,包括上面代码中的 formControl 指令,都会调用 setUpControl 函数来让表单控件和DefaultValueAccessor 实现交互(译者注:意思就是上面代码中绑定的 formControl 指令,在其自身实例化时,会调用 setUpControl() 函数给同样绑定到 input 的 DefaultValueAccessor 指令做好安装工作,如 L85,这样 formControl 指令就可以借助 DefaultValueAccessor 来和 input 元素交换数据了)。细节可参考 formControl 指令的代码:

export class FormControlDirective ... {
  ...
  ngOnChanges(changes: SimpleChanges): void {
    if (this._isControlChanged(changes)) {
      setUpControl(this.form, this);

还有 setUpControl 函数源码也指出了原生表单控件和 Angular 表单控件是如何数据同步的(译者注:作者贴的可能是 Angular v4.x 的代码,v5 有了点小小变动,但基本相似):

export function setUpControl(control: FormControl, dir: NgControl) {
  
  // initialize a form control
  // 调用 writeValue() 初始化表单控件值
  dir.valueAccessor.writeValue(control.value);
  
  // setup a listener for changes on the native control
  // and set this value to form control
  // 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新
  valueAccessor.registerOnChange((newValue: any) => {
    control.setValue(newValue, {emitModelToViewChange: false});
  });

  // setup a listener for changes on the Angular formControl
  // and set this value to the native control
  // 设置 Angular 表单控件值更新监听器,每当 Angular 表单控件值更新,原生控件值也更新
  control.registerOnChange((newValue: any, ...) => {
    dir.valueAccessor.writeValue(newValue);
  });

只要我们理解了内部机制,就可以实现我们自定义的 Angular 表单控件了。

组件封装器

由于 Angular 为所有默认原生控件提供了控件值访问器,所以在封装第三方插件或组件时,需要写一个新的控件值访问器。我们将使用上文提到的 jQuery UI 库的 slider 插件,来实现一个自定义表单控件吧。

简单的封装器

最基础实现是通过简单封装使其能在屏幕上显示出来,所以我们需要一个 NgxJquerySliderComponent 组件,并在其模板里渲染出 slider:

@Component({
  selector: 'ngx-jquery-slider',
  template: `
      <div #location></div>
  `,
  styles: ['div {width: 100px}']
})
export class NgxJquerySliderComponent {
  @ViewChild('location') location;
  widget;
  ngOnInit() {
    this.widget = $(this.location.nativeElement).slider();
  }
}

这里我们使用标准的 jQuery 方法在原生 DOM 元素上创建一个 slider 控件,然后使用 widget 属性引用这个控件。

一旦简单封装好了 slider 组件,我们就可以在父组件模板里使用它:

@Component({
  selector: 'my-app',
  template: `
      <h1>Hello {{name}}</h1>
      <ngx-jquery-slider></ngx-jquery-slider>
  `
})
export class AppComponent { ... }

为了运行程序我们需要加入 jQuery 相关依赖,简化起见,在 index.html 中添加全局依赖:

<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">

这里是安装依赖的 源码

交互式表单控件

上面的实现还不能让我们自定义的 slider 控件与父组件交互,所以还得使用输入/输出绑定来是实现组件间数据通信:

export class NgxJquerySliderComponent {
  @ViewChild('location') location;
  @Input() value;
  @Output() private valueChange = new EventEmitter();
  widget;

  ngOnInit() {
    this.widget = $(this.location.nativeElement).slider();   
    this.widget.slider('value', this.value);
    this.widget.on('slidestop', (event, ui) => {
      this.valueChange.emit(ui.value);
    });
  }

  ngOnChanges() {
    if (this.widget && this.widget.slider('value') !== this.value) {
      this.widget.slider('value', this.value);
    }
  }
}

一旦 slider 组件创建,就可以订阅 slidestop 事件获取变化的值,一旦 slidestop 事件被触发了,就可以使用输出事件发射器 valueChanges 通知父组件。当然我们也可以使用 ngOnChanges 生命周期钩子来追踪输入属性 value 值的变化,一旦其值变化,我们就将该值设置为 slider 控件的值。

然后就是父组件中如何使用 slider 组件的代码实现:

<ngx-jquery-slider
    [value]="sliderValue"
    (valueChange)="onSliderValueChange($event)">
</ngx-jquery-slider>

源码 在这里。

但是,我们想要的是,使用 slider 组件作为表单的一部分,并使用模板驱动表单或响应式表单的指令与其数据通信,那就需要让其实现 ControlValueAccessor 接口了。由于我们将实现的是新的组件通信方式,所以不需要标准的输入输出属性绑定方式,那就移除相关代码吧。(译者注:作者先实现标准的输入输出属性绑定的通信方式,又要删除,主要是为了引入新的表单组件交互方式,即 ControlValueAccessor。)

实现自定义控件值访问器

实现自定义控件值访问器并不难,只需要两步:

  1. 注册 NG_VALUE_ACCESSOR 提供者
  2. 实现 ControlValueAccessor 接口

NG_VALUE_ACCESSOR 提供者用来指定实现了 ControlValueAccessor 接口的类,并且被 Angular 用来和 formControl 同步,通常是使用组件类或指令来注册。所有表单指令都是使用NG_VALUE_ACCESSOR 标识来注入控件值访问器,然后选择合适的访问器(译者注:这句话可参考这两行代码,L175L181)。要么选择DefaultValueAccessor 或者内置的数据访问器,否则 Angular 将会选择自定义的数据访问器,并且有且只有一个自定义的数据访问器(译者注:这句话参考 selectValueAccessor 源码实现)。

让我们首先定义提供者:

@Component({
  selector: 'ngx-jquery-slider',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: NgxJquerySliderComponent,
    multi: true
  }]
  ...
})
class NgxJquerySliderComponent implements ControlValueAccessor {...}

我们直接在组件装饰器里直接指定类名,然而 Angular 源码默认实现是放在类装饰器外面:

export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DefaultValueAccessor),
  multi: true
};
@Directive({
  selector:'input',
  providers: [DEFAULT_VALUE_ACCESSOR]
  ...
})
export class DefaultValueAccessor implements ControlValueAccessor {}

放在外面就需要使用 forwardRef,关于原因可以参考 What is forwardRef in Angular and why we need it 。当实现自定义 controlValueAccessor,我建议还是放在类装饰器里吧(译者注:个人建议还是学习 Angular 源码那样放在外面)。

一旦定义了提供者后,就让我们实现 controlValueAccessor 接口:

export class NgxJquerySliderComponent implements ControlValueAccessor {
  @ViewChild('location') location;
  widget;
  onChange;
  value;
  
ngOnInit() {
	this.widget = $(this.location.nativeElement).slider(this.value);
   this.widget.on('slidestop', (event, ui) => {
      this.onChange(ui.value);
    });
}
  
writeValue(value) {
    this.value = value;
    if (this.widget && value) {
      this.widget.slider('value', value);
    }
  }
  
registerOnChange(fn) { this.onChange = fn;  }

registerOnTouched(fn) {  }

由于我们对用户是否与组件交互不感兴趣,所以先把 registerOnTouched 置空吧。在registerOnChange 里我们简单保存了对回调函数 fn 的引用,回调函数是由 formControl 指令传入的(译者注:参考 L85),只要每次 slider 组件值发生改变,就会触发这个回调函数。在 writeValue 方法内我们把得到的值传给 slider 组件。

现在我们把上面描述的功能做成一张交互式图:

image

如果你把简单封装和 controlValueAccessor 封装进行比较,你会发现父子组件交互方式是不一样的,尽管封装的组件与 slider 组件的交互是一样的。你可能注意到 formControl 指令实际上简化了与父组件交互的方式。这里我们使用 writeValue 来向子组件写入数据,而在简单封装方法中使用 ngOnChanges;调用 this.onChange 方法输出数据,而在简单封装方法中使用 this.valueChange.emit(ui.value)。

现在,实现了 ControlValueAccessor 接口的自定义 slider 表单控件完整代码如下:

@Component({
  selector: 'my-app',
  template: `
      <h1>Hello {{name}}</h1>
      <span>Current slider value: {{ctrl.value}}</span>
      <ngx-jquery-slider [formControl]="ctrl"></ngx-jquery-slider>
      <input [value]="ctrl.value" (change)="updateSlider($event)">
  `
})
export class AppComponent {
  ctrl = new FormControl(11);

  updateSlider($event) {
    this.ctrl.setValue($event.currentTarget.value, {emitModelToViewChange: true});
  }
}

你可以查看程序的 最终实现

Github

项目的 Github 仓库

原文: https://blog.angularindepth.com/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms-93b9eee9ee83

Angular Ivy的变更检测执行:你准备好了吗?

让我们看看Angular为我们做了什么。

声明:这只是我对Angular新渲染器的学习之旅。

Angular视图引擎的演变
Angular视图引擎的演变

虽然新的Ivy渲染器的重要性还没有完全展现出来,但许多人想知道它将如何工作以及它为我们准备的变化。

在本文中,我将展示Ivy变更检测机制,展示一些让我非常兴奋的事情,并从头开始,根据指导(类似于Angular Ivy指导)构建简单的app。

首先,介绍一下我下面将研究的app:
app

@Component({
  selector: 'my-app',
  template: `
   <h2>Parent</h2>
   <child [prop1]="x"></child>
  `
})
export class AppComponent {
  x = 1;
}
@Component({
  selector: 'child',
  template: `
   <h2>Child {{ prop1 }}</h2>
   <sub-child [item]="3"></sub-child>
   <sub-child *ngFor="let item of items" [item]="item"></sub-child>
  `
})
export class ChildComponent {
  @Input() prop1: number;
  
  items = [1, 2];
}
@Component({
  selector: 'sub-child',
  template: `
   <h2 (click)="clicked.emit()">Sub-Child {{ item }}</h2>
   <input (input)="text = $event.target.value">
   <p>{{ text }}</p>
  `
})
export class SubChildComponent {
  @Input() item: number;
  @Output() clicked = new EventEmitter();
  text: string;
}

我创建了一个在线demo,用于了解Ivy如何在幕后运行:
https://alexzuza.github.io/ivy-cd/

lvy

Demo使用了Angular 6.0.1 aot 编译器。你可以单击任何生命周期块来跳转到对应的代码。

为了运行变更检测过程,只需在Sub-Child下面的输入框中输入一些内容即可。

视图

当然,视图是Angular中主要的低级抽象。

对于我们的例子,我们会得到下面类似的结构:

Root view
   |
   |___ AppComponent view
          |
          |__ ChildComponent view
                 |
                 |_ Embedded view
                 |       |
                 |       |_ SubChildComponent view
                 |
                 |_ Embedded view
                 |       |
                 |       |_ SubChildComponent view   
                 |
                 |_ SubChildComponent view       

视图应该描述模板,以及它包含一些反映该模板结构的数据。

我们来看看ChildComponent视图。它有以下模板:

<h2>Child {{ prop1 }}</h2>
<sub-child [item]="3"></sub-child>
<sub-child *ngFor="let item of items" [item]="item"></sub-child>

当前视图引擎从视图定义工厂创建nodes并将它们存储在视图定义的nodes数组中。

Ivy从instructions创建LNodes,这个instructions被写入ngComponentDef.template函数,并将它们存储在data数组中:

除了nodes之外,新视图还包含data数组中的绑定(参见上图中的data[4],data[5],data[6])。给定视图的所有绑定,从bindingStartIndex开始按照它们出现在模板中的顺序进行存储。

注意我如何从ChildComponent获取视图实例。 ComponentInstance . ngHostLNode包含对组件宿主节点的引用。 (另一种方法是注入ChangeDetectorRef)

在这种方式下,angular 会首先创建根视图,并在data数组索引0处定位宿主元素

RootView
   data: [LNode]
             native: root component selector

然后遍历所有组件并为每个视图填充data数组。

变更检测

众所周知,ChangeDetectorRef只是抽象类,具有诸如detectChanges,markForCheck等抽象方法。

当我们在组件构造函数中询问这个依赖关系时,我们实际上得到了继承 ChangeDetectorRef 类的ViewRef实例。

现在,我们来看看用于在Ivy中运行变更检测的内部方法。其中一些可用作公共API(markViewDirty和detectChanges),但我不确定其他的API。

detectChanges

detectChanges 是对组件(及其可能的子组件)同步执行变更检测。

这个函数在组件中以同步方式触发变更检测。应该没有什么理由直接调用此函数,执行变更检测的首选方法是使用markDirty(请参见下文),并等待调度程序在将来某个时间点调用此方法。这是因为单个用户操作通常会导致许多组件失效,并且在每个组件上同步调用变更检测效率低下。最好等到所有组件都标记为脏,然后在所有组件上执行单一变更检测。

tick

用于在整个应用程序上执行变更检测。

这相当于detectChanges,但是要在根组件上调用。另外,tick执行生命周期钩子,并根据它们的ChangeDetectionStrategy和dirtiness来有条件地检查组件。

export function detectChanges<T>(component: T): void {
  const hostNode = _getComponentHostLElementNode(component);
  ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView');
  const componentIndex = hostNode.tNode !.flags >> TNodeFlags.DirectiveStartingIndexShift;
  const def = hostNode.view.tView.directives ![componentIndex] as ComponentDef<T>;
  detectChangesInternal(hostNode.data as LView, hostNode, def, component);
}

scheduleTick

用于安排整个应用程序的变更检测。与tick不同,scheduleTick将多个调用合并为一个变更检测运行。当视图需要重新渲染时,通常通过调用markDirty间接调用它。

export function tick<T>(component: T): void {
  const rootView = getRootView(component);
  const rootComponent = (rootView.context as RootContext).component;
  const hostNode = _getComponentHostLElementNode(rootComponent);

  ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView');
  renderComponentOrTemplate(hostNode, rootView, rootComponent);
}

markViewDirty(markForCheck)

标记当前视图和所有祖先视图为脏(译者注:脏为需要变更检测)。

在早期的Angular 5中,它只向上迭代并启用了所有父视图的检查,现在请注意,markForCheck的确触发了Ivy变更检测周期! 😮😮😮

markDirty

将组件标记为脏。

标记为脏的组件将在未来的某个时间安排对此组件进行变更检测。将一个已经为脏的组件标记为脏是一个空操作。每个组件树只能安排一次未完成的变更检测。 (使用单独的renderComponent引导的两个组件将具有单独的调度器)

checkNoChanges

没变化:)

当我调试新的变更检测机制时,我注意到我忘记了安装zone.js。而且,正如你已经猜到的一样,它没有依赖性,没有cdRef.detectChanges或tick,它依然完美运行。

为什么呢?

你可能知道Angular只会对onPush组件触发变更检测(请参阅我在stackoverflow上的回答)。

这些规则同样适用于Ivy:

其中一个输入发生变化
https://github.com/angular/angular/blob/43d62029f0e2da0150ba6f09fd8989ca6391a355/packages/core/src/render3/instructions.ts#L890

由组件或其子组件触发的绑定事件
https://github.com/angular/angular/blob/43d62029f0e2da0150ba6f09fd8989ca6391a355/packages/core/src/render3/instructions.ts#L1743

手动调用markForCheck
(现在用markViewDirty函数(见下文))

在SubChildComponent中,有(input)output绑定。第二条规则将导致调用markForCheck。既然我们已经知道这个方法实际上调用变更检测,现在应该清楚它如何在没有zonejs的情况下工作。

如果在检测后表达式变化了怎么办?

不要着急,它还在

变更检测顺序

自从发布Ivy以来,Angular团队一直在努力确保新引擎以正确的顺序正确处理所有生命周期钩子。这意味着操作顺序应该是相似的。

Max NgWizard K在他的文章中写道(强烈建议阅读它):

正如你所看到的,所有熟悉的操作仍然在这里。但操作顺序似乎已经改变。例如,现在看来Angular首先检查子组件,然后检查嵌入的视图。由于目前没有编译器能够产生适合测试我假设的输出,所以这点我无法确定。

回到刚刚demo的子组件中来:

<h2>Child {{ prop1 }}</h2>
<sub-child [item]="3"></sub-child>
<sub-child *ngFor="let item of items" [item]="item"></sub-child>

我打算在其他内嵌视图之前写一个sub-child作为常规组件。

现在观察它的运行:

angular首先检查嵌入视图,然后检查常规组件。所以这里和以前的引擎相比没有改变。

无论如何,我的演示中有可选的“run Angular compile”按钮,我们可以测试其他情况。

alexzuza.github.io/ivy-cd/

一次性字符串初始化

想象一下,我们写了可以接收颜色作为字符串输入值的组件。现在我们想把这个输入作为永远不会改变的常量字符串来传递:

<comp color="#efefef"></comp>

这就是所谓的一次性字符串初始化,angular文档中的陈述如下:

Angular 设置它,然后忘记它。

对我而言,这意味着 angular 不会对此绑定进行任何额外的检查。但是我们在 angular5 中实际看到的是,它在 updateDirectives 调用期间,每一次变更检测期间就会检查一次。

function updateDirectives(_ck,_v) {
   var currVal_0 = '#efefef';
  _ck(_v,1,0,currVal_0);

另请参阅Netanel Basal的关于此问题的文章了解Angular的@Attribute装饰器

现在让我们看看它在新的引擎中是怎么样的:

var _c0 = ["color", "#efefef"];
AppComponent.ngComponentDef = i0.ɵdefineComponent({ 
  type: AppComponent,
  selectors: [["my-app"]], 
  ...
  template: function AppComponent_Template(rf, ctx) { 
    // create mode
      if (rf & 1) {
        i0.ɵE(0, "child", _c0); <========== used only in create mode
        i0.ɵe();
      }
      if (rf & 2) {
        ...
      }
  }
})

正如我们所看到的,angular编译器将常量存储在负责创建和更新组件的代码之外,并且只在创建模式下使用此值。

Angular不再为容器创建文本节点

更新: angular/angular#24346

即使你不知道angular ViewContainer 在引擎中如何工作,你在打开devtools时可能会注意到下面的图片:

在生产模式下,我们只看到<!—>。

这是Ivy的输出

我无法100%确定,但似乎一旦Ivy变得稳定,我们就会有这样的结果。

因此对于下面的代码中query,angular将返回null

@Component({
  ...,
  template: '<ng-template #foo></ng-template>'
})
class SomeComponent {
  @ViewChild('foo', {read: ElementRef}) query;
}

应该不再使用指向容器中的注释DOM节点的本地元素读取ElementRef

全新的 Incremental DOM(IDOM)

很久以前,Google发布了所谓的Incremental DOM库。

该库专注于构建DOM树并允许动态更新。它不能直接使用,而是作为模板引擎的编译目标。而且似乎Ivy与Incremental DOM库有一些共同之处。

让我们从头开始构建一个简单的app,这将帮助我们了解IDOM渲染如何工作的。Demo

我们的app将有计数器,并会把通过input元素输入的用户名打印出来。

假设页面上已经有和元素:

<input type="text" value="Alexey">
<button>Increment</button>

我们需要做的只是渲染动态html,看起来像这样:

<h1>Hello, Alexey</h1>
<ul>
  <li>
    Counter: <span>1</span>
  </li>
</ul>

为了渲染这些,让我们编写elementOpen,elementClose和文本“instructions”(我这样称呼它,因为Angular使用像Ivy这样的名称可以被认为是特殊类型的虚拟CPU)。

首先,我们需要编写特殊的助手来遍历节点树:

// The current nodes being processed
let currentNode = null;
let currentParent = null;

function enterNode() {
  currentParent = currentNode;
  currentNode = null;
}
function nextNode() {
  currentNode = currentNode ? 
    currentNode.nextSibling : 
    currentParent.firstChild;
}
function exitNode() {
  currentNode = currentParent;
  currentParent = currentParent.parentNode;
}

现在让我们写instructions:

function renderDOM(name) {
  const node = name === '#text' ? 
  	document.createTextNode('') :
    document.createElement(name);
  currentParent.insertBefore(node, currentNode);
  currentNode = node;
  return node;
}
function elementOpen(name) {
  nextNode();
  const node = renderDOM(name);
  enterNode();
  return currentParent;
}
function elementClose(node) {
  exitNode();
  return currentNode;
}
function text(value) {
  nextNode();
  const node = renderDOM('#text');
  node.data = value;
  return currentNode;
}

换句话说,这些函数只是遍历DOM节点并在当前位置插入节点。此外,文本命令设置data属性,以便我们可以看到浏览器的文本值。

我们希望我们的元素能够保持某种状态,所以我们来介绍NodeData:

const NODE_DATA_KEY = '__ID_Data__';
class NodeData {
  // key
  // attrs
  
  constructor(name) {
    this.name = name;
    this.text = null;
  }
}
function getData(node) {
  if (!node[NODE_DATA_KEY]) {
    node[NODE_DATA_KEY] = new NodeData(node.nodeName.toLowerCase());
  }
  return node[NODE_DATA_KEY];
}

现在,让我们改动一下renderDOM函数,以便在当前位置已经相同的情况下,我们不会向DOM添加新元素:

const matches = function(matchNode, name/*, key */) {
  const data = getData(matchNode);
  return name === data.name // && key === data.key;
};
function renderDOM(name) {
  if (currentNode && matches(currentNode, name/*, key */)) {
    return currentNode;
  }
  ...
}

注意我注释的 /*, key */。如果元素有key来区分元素会更好。另请参阅http://google.github.io/incremental-dom/#demos/using-keys

之后,让我们添加将负责文本节点更新的逻辑:

function text(value) {
  nextNode();
  const node = renderDOM('#text');
  
  // update
  // checks for text updates
  const data = getData(node);
  if (data.text !== value) {
    data.text = (value);
    node.data = value;
  }
  // end update
  
  return currentNode;
}

我们可以为元素节点做同样的事情。

然后,让我们来编写patch函数,它将需要DOM元素,update函数以及一些数据(这些数据将由update函数使用):

function patch(node, fn, data) {
  currentNode = node;
  enterNode();
  fn(data);
  exitNode();
};

最后,让我们测试一下这个instructions:

function render(data) {
  elementOpen('h1');
  {
    text('Hello, ' + data.user)
  }
  elementClose('h1');
  elementOpen('ul')
  {
    elementOpen('li'); 
    {
      text('Counter: ')
      elementOpen('span'); 
      {
        text(data.counter);
      }
      elementClose('span');
    }
    elementClose('li');
  }
  elementClose('ul');
}
document.querySelector('button').addEventListener('click', () => {
   data.counter ++;
   patch(document.body, render, data);
});
document.querySelector('input').addEventListener('input', (e) => {
   data.user = e.target.value;
   patch(document.body, render, data);
});
const data = {
  user: 'Alexey',
  counter: 1
};
patch(document.body, render, data);

结果可以在这找到。https://jsfiddle.net/yurzui/hqhq4khc

你还可以通过使用浏览器工具,来验证代码是否仅更新其内容已更改的文本节点:

所以IDOM的主要理念就是使用真正的DOM来和新树进行对比。

全文完。谢谢阅读。

https://blog.angularindepth.com/angular-ivy-change-detection-execution-are-you-prepared-ab68d4231f2c

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.