Angular 4.x 修仙之路我爱编程

Angular数据检测及单向数据流

2017-06-04  本文已影响352人  bacbcc94613b

本文学习总结于

数据

在Angular中,我们所说的数据,即组件持有的数据模型。

import { Component } from '@angular/core';
import {Course} from "./course";

export interface Course {
    id:number;
    description:string;
}

@Component({
  selector: 'app-root',
  template: `
    <div class="course">
        <span class="description">{{course.description}}</span>
    </div>
`})
export class AppComponent {


    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };
    
}

Angular通过模板把数据模型转换成视图。

模板在Angular内部的使用

Angular应用中,模板指的的是@Component装饰器的template或templateUrl指向的HTML页面
例如:

@Component({
  selector: 'app-root',
  template: `
    <div class="course">
        <span class="description">{{course.description}}</span>
    </div>
`})
export class AppComponent {


    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };
    
}

在模板出现问题时,比如少了一个关闭标签,在控制台我们会收到有用的错误消息。 所以很明显Angular不是简单地用一个字符串来处理模板。 那么这是如何工作的?
在Angular中,Angular并不是把数据通过替换一些变量来创建基于模板的实际HTML然后将此HTML传递给浏览器然后浏览器解析HTML并生成DOM数据结构浏览器渲染引擎然后在屏幕上呈现视图。

Angular不会生成HTML字符串,它直接生成DOM数据结构。

实际上,Angular把组件类中的数据模型应用于一个函数(DOM component renderer)。 该函数的输出是对应于此HTML模板的DOM数据结构。

该函数的定义大体如下:

View_AppComponent_0.prototype.createInternal = function(rootSelector) {
        var self = this;
        
        var parentRenderNode = self.renderer.createViewRoot(self.parentElement);
        
        self._text_0 = self.renderer.createText(parentRenderNode,'\n',self.debug(0,0,0));
        
        self._el_1 = jit_createRenderElement5(self.renderer,parentRenderNode,'div',
               new jit_InlineArray26(2,'class','course'),self.debug(1,1,0));
        
        self._text_2 = self.renderer.createText(self._el_1,'\n\n    ',self.debug(2,1,20));
        
        self._el_3 = jit_createRenderElement5(self.renderer,self._el_1,'span',
              new jit_InlineArray26(2,'class','description'),self.debug(3,3,4));
        
        self._text_4 = self.renderer.createText(self._el_3,'',self.debug(4,3,30));
        
        self._text_5 = self.renderer.createText(self._el_1,'\n\n',self.debug(5,3,59));
        
        self._text_6 = self.renderer.createText(parentRenderNode,'\n',self.debug(6,5,6));
        
        self.init(null,(self.renderer.directRenderer? null: [
                self._text_0,
                self._el_1,
                self._text_2,
                self._el_3,
                self._text_4,
                self._text_5,
                self._text_6
            ]
        ),null);
        return null;
    };

从其中createViewRoot,createText一些方法和parentElement,parentRenderNode命名,可以大概知道该函数正在创建一个DOM数据。

一旦数据状态发生改变,Angular数据检测器检测到,将重新调用
该DOM component renderer。

如何查看自己的组件的DOM component renderer,以及该函数的产生时机,请参考Angular - What is Unidirectional Data Flow ? Learn How the Angular Development Mode Works, why it's important to use it and how to Troubleshoot it中的Where can I find this function for my components ?When is this code generated ? 章节。

引起数据模型变化的来源

数据模型一旦发生改变,视图就要相应发生变化,这也是现在流行的Model Driven View。那么就客户端(浏览器)来说,引起数据模型发生变化的事件源有:

这些事件源有一个共同的特性,即它们都是异步操作。那我们可以这样认为,所有的异步操作都有可能会引起模型的变化。

变更检测和单向数据流规则

每一个异步操作都有可能引起数据状态的变更, Angular封装 Zone来拦截跟踪异步(这里不对Zone进行解释说明,请自行查阅)。

在Angular中,单向数据流规则是指当数据模型发生变化,Angular发动变更检测,调用DOM ompoent render把数据模型转化为DOM数据结构,应用中的数据只会单向转向成DOM数据结构,不可发生其他改变的方向。

单向数据流规则

注:Angular从组件树的顶部到底部的整个渲染扫描过程也是单向的。

Angular为什么要遵循单向数据流规则呢?

为什么要单向数据流?

在AngularJS中,数据的流动是双向,稍微复杂的情况下,这种流动会变得不可预测,有可能到导致整个应用陷入“无限震荡”中。

我们希望确保在将数据转换为视图的过程中,不会进一步修改数据。数据从组件类流向代表它们的DOM数据结构,生成这些DOM数据结构的行为本身不会对数据进行进一步修改。但在Angular的变更检测周期中,组件的生命周期钩子会被调用,这意味着我们编写的代码在该过程中被调用,该代码有可能引发数据状态发生改变。

Angular组件生命周期

例如

import {Component, AfterViewChecked} from '@angular/core';
import {Course} from "./course";

@Component({
    selector: 'app-root',
    template: `
    <div class="course">
        <span class="description">{{course.description}}</span>
    </div>
`})
export class AppComponent implements AfterViewChecked {

    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };

    ngAfterViewChecked() {
        this.course.description += Math.random();
    }

}

上述代码会在Angular变更检测周期发生错误。我们在该组件ngAfterViewChecked()方法中修改了数据状态。导致了视图渲染后,数据跟视图状态不一致。

解决:

ngAfterViewChecked() {
    setTimeout(() => {
        this.course.description += Math.random();
    });
}

我们可以使用setTimeout将数据修改延迟到下一个变更周期。

除了组件生命周期回调的钩子可能触发数据状态的改变还有其他,
例如

import { Component } from '@angular/core';
import {Course} from "./course";

@Component({
    selector: 'app-root',
    template: `
    <div class="course">
        <span class="description">{{description}}</span>
    </div>
`})
export class AppComponent {

    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };

    get description() {
        return this.course.description + Math.random();
    }
}

Angular每次检测description时,它都会返回一个不同值。

在Angular变更检测周期,任意会改变数据状态的行为都会抛出异常从而终止。

如果Angular没有制止该行为,数据和视图会保持在不一致的状态,其中渲染过程完成后的视图不反映数据的实际状态。或者重复检测,直到数据稳定可能会导致性能问题。

单向数据流重要性

变化检测性能优化

变化检测前:

变化检测前

变化检测时:

变化检测时

每次变化检测都是从根组件开始,从上往下执行,遍历每个组件。
由于框架已经自动为模版生成的代码做了非常多的优化,即使是在未使用优化过的 model 的情况下都已经可以达到 ng 1脏检测性能的 3-10 倍(同样绑定数量,同样检测次数)。视频中提到了这是因为 ng 2 在生成模版代码时,会动态生成让 js 引擎易于优化的代码,大概原理就是保持每次 check change 前后对象“形状 ”的一致。而如果在性能有瓶颈的地方,可以使用下面两种方式进行高阶优化:

OnPush变化检测策略

OnPush 策略:若输入属性没有发生变化,组件的变化检测将会被跳过

OnPush变化检测策略+Immutable

Angular对复杂数据类型即对象的检测只是检测对该对象的引用是否改变

当对象属性值改变,但对其引用没改变,Angular会默该改数据没发生变化。

实践例子可参考:Angular 2 Change Detection - 2 的OnPush策略章节。

因此当我们使用 OnPush 策略时,需要使用的 Immutable 的数据结构(Immutable 即不可变,表示当数据模型发生变化的时候,我们不会修改原有的数据模型,而是创建一个新的数据模型),才能保证程序正常运行。

为了提高变化检测的性能,我们应该尽可能在组件中使用 OnPush 策略,为此我们组件中所需的数据,应仅依赖于输入属性

OnPush变化检测策略+Observable

使用 immutable 时 change detection cycle 依旧从 root component 开始往下,依次检测。

上图每个 component 都使用了 immutable model ,白色的部分是变更的部分,则在一个 change detection cycle 中只会 recheck & render 白色的部分,从而大大减少处理变更的代价。

而使用 OnPush变化检测策略 和 Observable 时,情况就不一样了,它的变更很可能是从一个非常下层的子 component 中开始发生的,比如:

在图中一个子 component 通过 observable 观察到了一次数据的变更。这个时候我们需要告知 Angular 这个部分发生了变更,它将会把这个 component 与它的父 component 一直到 root component 标记出来,并单独检测这一部分的变更:

ChangeDetectorRef

ChangeDetectorRef 是组件的变化检测器的引用,我们可以在组件中的通过依赖注入的方式来获取该对象,来手动控制组件的变化检测行为:

ChangeDetectorRef 变化检测类中主要方法有以下几个:

export abstract class ChangeDetectorRef {
  abstract markForCheck(): void;
  abstract detach(): void;
  abstract detectChanges(): void;
  abstract reattach(): void;
}

其中各个方法的功能介绍如下:

import { Component, Input, OnInit, ChangeDetectionStrategy, 
         ChangeDetectorRef } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Component({
    selector: 'exe-counter',
    template: `
      <p>当前值: {{ counter }}</p>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
    counter: number = 0;

    @Input() addStream: Observable<any>;

    constructor(private cdRef: ChangeDetectorRef) { }

    ngOnInit() {
        this.addStream.subscribe(() => {
            this.counter++;
            this.cdRef.markForCheck();
        });
    }
}

总结

在此,非常感谢 semlinker 的帮助!

上一篇 下一篇

猜你喜欢

热点阅读