>웹 프론트엔드 >JS 튜토리얼 >Angular의 변경 감지에 대한 심층적인 대화를 안내해 드리겠습니다.

Angular의 변경 감지에 대한 심층적인 대화를 안내해 드리겠습니다.

青灯夜游
青灯夜游앞으로
2022-02-14 10:17:592175검색

이 글에서는 Angular의 변경 감지에 대해 심도 있게 이야기하고, Angular가 비동기 이벤트를 구독하여 변경 감지를 수행하는 방법을 소개하고, 변경 감지 전략에 대해 이야기하겠습니다.

Angular의 변경 감지에 대한 심층적인 대화를 안내해 드리겠습니다.

변경 감지는 프런트 엔드 프레임워크에서 매우 흥미로운 부분입니다. 각 프런트 엔드 프레임워크에는 자체 솔루션 세트도 있습니다. 일반적으로 변경 감지에 대해 너무 많이 알 필요는 없습니다. 프레임워크는 이미 대부분의 작업을 수행했습니다. 그러나 프레임워크를 심층적으로 사용하면 변경 감지를 이해하지 못하는 것이 어렵다는 것을 알게 됩니다. 변경 감지를 이해하면 프레임워크를 더 잘 이해하고 오류를 해결하며 성능 최적화를 수행하는 데 도움이 됩니다. [관련 튜토리얼 추천: "angular 튜토리얼"]

변경 감지란 무엇인가요?

간단히 말하면, 변경 감지는 뷰와 상태 간의 변화를 감지하여 뷰를 동기화하는 메커니즘으로 상태 변경 후 업데이트하는 데 도움이 됩니다. 우리 데이터를 사용하는 것을 변경 감지라고 합니다.

변경 감지 트리거 시간

변경 감지가 무엇인지는 알지만, 변경 감지는 언제 트리거되나요? 다음 두 가지 간단한 Demo

Demo1:

A 카운터 구성 요소를 살펴볼 수 있습니다. 버튼을 클릭하면 Count는 항상 1

@Component({
  selector: "app-counter",
  template: `
    Count:{{ count }}
    <br />
    <button (click)="increase()">Increase</button>
  `,
})
export class CounterComponent {
  count = 0;
 
  constructor() {}
 
  increase() {
    this.count = this.count + 1;
  }
}

Demo2:

A Todo List 구성 요소로 증가하며 HTTP를 통해 데이터를 가져오고 렌더링합니다.

  @Component({
    selector: "app-todos",
    template: ` <li *ngFor="let item of todos">{{ item.titme }}</li> `,
  })
  export class TodosComponent implements OnInit {
    public todos: TodoItem[] = [];

    constructor(private http: HttpClient) {}

    ngOnInit() {
      this.http.get<TodoItem[]>("/api/todos").subscribe((todos: TodoItem[]) => {
        this.todos = todos;
      });
    }
  }

위의 두 데모에서 우리는 두 가지 상황에서 변경 감지가 트리거되는 것을 발견했습니다.

  • 클릭 이벤트가 발생할 때

  • http를 통해 원격 데이터가 요청될 때

생각해 보세요 두 가지 트리거링 방법의 공통점은 무엇입니까? 두 방법 모두 비동기 작업이라는 것을 알았으므로 결론을 내릴 수 있습니다. 비동기 작업이 발생하는 한 Angular는 상태가 변경되었을 수 있다고 생각합니다. 그러면 변경 감지가 수행됩니다.

이때 setTimeoutsetInterval을 생각해 보세요. 예, 변경 감지도 트리거됩니다. setTimeout  setInterval   ,是的,它们同样也会触发变化检测。

@Component({
  selector: "app-counter",
  template: `
    Count:{{ count }}
    <br />
    <button (click)="increase()">Increase</button>
  `,
})
export class CounterComponent implements OnInit {
  count = 0;

  constructor() {}
  
  ngOnInit(){
    setTimeout(()=>{
       this.count= 10;
    });
  }

  increase() {
    this.count = this.count + 1;
  }
}

简而言之,如果发生以下事件之一,Angular 将触发变化检测:

  • 任何浏览器事件(click、keydown 等)

  • setInterval()  和  setTimeout()

  • HTTP 通过  XMLHttpRequest  进行请求

Angular 如何订阅异步事件执行变化检测?

刚才我们了解到,只要发生了异步操作,Angular 就会进行变化检测,那 Angular 又是如何订阅到异步事件的状态,从而触发变化检测的呢?这里我们就要聊一聊 zone.js 了。

Zone.js

Zone.js 提供了一种称为 ** 区域(Zone) ** 的机制,用于封装和拦截浏览器中的异步活动、它还提供 异步生命周期的钩子 和 统一的异步错误处理机制。

Zone.js 是通过 Monkey Patching(猴子补丁) 的方式来对浏览器中的常见方法和元素进行拦截,例如 setTimeout 和 HTMLElement.prototype.onclick 。Angular 在启动时会利用 zone.js 修补几个低级浏览器 API,从而实现异步事件的捕获,并在捕获时间后调用变化检测。

下面用一段简化的代码来模拟一下替换 setTimeout 的过程:

function setTimeoutPatch() {
  // 存储原始的setTimeout
  var originSetTimeout = window[&#39;setTimeout&#39;];
  // 对浏览器原生方法的包裹封装
  window.setTimeout = function () {
      return global[&#39;zone&#39;][&#39;setTimeout&#39;].apply(global.zone, arguments);
  };
  // 创建包裹方法,提供给上面重写后的setTimeout使用Ï
  Zone.prototype[&#39;setTimeout&#39;] = function (fn, delay) {
    // 先调用原始方法
    originSetTimeout.apply(window, arguments);
    // 执行完原始方法后就可以做其他拦截后需要进行的操作了
    ...
   };
}

NgZone

Zone.js 提供了一个全局区域,可以被 fork 和扩展以进一步封装/隔离异步行为,Angular 通过创建一个fork并使用自己的行为扩展它,通常来说, 在 Angular APP 中,每个 Task 都会在 Angular 的 Zone 中运行,这个 Zone 被称为  NgZone 。一个 Angular APP 中只存在一个 Angular Zone, 而变更检测只会由运行于这个 ** **NgZone**

@Component({
  selector: "app-todos",
  ...
})
export class TodosComponent{
  constructor(cdr: ChangeDetectorRef) {}
}
간단히 말하면, Angular는 다음 이벤트 중 하나가 발생하면 변경 감지를 트리거합니다:

모든 브라우저 이벤트(클릭, 키다운 등)

setInterval() 및 setTimeout()HTTP는 XMLHttpRequest

Angular를 통해 요청합니다. 변경 감지를 수행하기 위해 비동기 이벤트를 구독하는 방법은 무엇입니까? 🎜🎜🎜지금 우리는 비동기 작업이 발생하는 한 Angular가 변경 감지를 수행한다는 것을 배웠습니다. 그렇다면 Angular는 변경 감지를 트리거하기 위해 비동기 이벤트의 상태를 어떻게 구독합니까? 여기서는 zone.js에 대해 이야기하겠습니다. 🎜🎜🎜🎜Zone.js🎜🎜🎜Zone.js는 캡슐화를 위해 **Zone**이라는 메커니즘을 제공하고 브라우저에서 비동기 활동을 차단합니다. 또한 🎜비동기 수명 주기 후크🎜 및 🎜통합 비동기 오류 처리 메커니즘도 제공합니다. 🎜🎜🎜Zone.js는 🎜Monkey Patching🎜을 사용하여 setTimeoutHTMLElement.prototype.onclick과 같은 브라우저의 일반적인 메서드와 요소를 가로챕니다. Angular는 zone.js를 활용하여 시작 시 여러 하위 수준 브라우저 API를 패치하여 비동기 이벤트를 캡처하고 캡처 시간 이후 변경 감지를 호출합니다. 🎜🎜다음은 setTimeout 교체 프로세스를 시뮬레이션하기 위한 단순화된 코드입니다. 🎜
class ApplicationRef {
  // ViewRef 是继承于 ChangeDetectorRef 的
  _views: ViewRef[] = [];
  constructor(private _zone: NgZone) {
    this._zone.onMicrotaskEmpty.subscribe({
      next: () => {
        this._zone.run(() => {
          this.tick();
        });
      },
    });
  }

  // 执行变化检测
  tick() {
    for (let view of this._views) {
      view.detectChanges();
    }
  }
}
🎜🎜NgZone🎜🎜🎜Zone.js는 전역 영역을 제공합니다. 비동기 동작을 추가로 캡슐화/격리하기 위해 분기되고 확장됩니다. 일반적으로 Angular APP에서 각 작업은 For NgZone'이라고 불리는 영역에서 실행됩니다. 코드>. Angular 앱에는 Angular 영역이 하나만 있으며 변경 감지는 이 ** <code>**NgZone** **에서 실행되는 비동기 작업에 의해서만 트리거됩니다. 🎜🎜간단히 이해하면 다음과 같습니다. 🎜Angular는 Zone.js를 통해 자체 영역을 만들고 이를 NgZone이라고 부릅니다. 🎜🎜🎜변경 감지는 어떻게 작동하나요? 🎜🎜Angular의 핵심은 🎜컴포넌트화🎜라는 것을 알고 있습니다. 컴포넌트의 중첩은 결국 🎜컴포넌트 트리🎜를 형성하게 됩니다. 🎜🎜🎜🎜

 Angular 在生成组件的同时,还会为每一个组件生成一个变化检测器 changeDetector ,用来记录组件的数据变化状态,由于一个 Component 会对应一个 changeDetector ,所以changeDetector 同样也是一个树状结构的组织。

 在组件中我们可以通过注入 ChangeDetectorRef  来获取组件的 changeDetector 

@Component({
  selector: "app-todos",
  ...
})
export class TodosComponent{
  constructor(cdr: ChangeDetectorRef) {}
}

我们在创建一个 Angular 应用 后,Angular 会同时创建一个 ApplicationRef  的实例,这个实例代表的就是我们当前创建的这个 Angular 应用的实例。 ApplicationRef 创建的同时,会订阅 ngZone 中的 onMicrotaskEmpty  事件,在所有的微任务完成后调用所有的视图的detectChanges()  来执行变化检测。

下是简化的代码:

class ApplicationRef {
  // ViewRef 是继承于 ChangeDetectorRef 的
  _views: ViewRef[] = [];
  constructor(private _zone: NgZone) {
    this._zone.onMicrotaskEmpty.subscribe({
      next: () => {
        this._zone.run(() => {
          this.tick();
        });
      },
    });
  }

  // 执行变化检测
  tick() {
    for (let view of this._views) {
      view.detectChanges();
    }
  }
}

单向数据流

什么是单向数据流?

刚才我们说了每次触发变化检测,都会从根组件开始,沿着整棵组件树从上到下的执行每个组件的变更检测,默认情况下,直到最后一个叶子 Component 组件完成变更检测达到稳定状态。在这个过程中,一但父组件完成变更检测以后,在下一次事件触发变更检测之前,它的子孙组件都不允许去更改父组件的变化检测相关属性状态的,这就是单向数据流。

我们看一个示例:

@Component({
  selector: "app-parent",
  template: `
    {{ title }}
    <app-child></app-child>
  `, 
})
export class ParentComponent {
  title = "我的父组件";
}

@Component({
  selector: "app-child",
  template: ``, 
})
export class ChildComponent implements AfterViewInit {
  constructor(private parent: ParentComponent) {}

  ngAfterViewInit(): void {
    this.parent.title = "被修改的标题";
  }
}

 为什么出现这个错误呢?

这是因为我们违反了单向数据流,ParentComponent 完成变化检测达到稳定状态后,ChildComponent 又改变了 ParentComponent 的数据使得 ParentComponent 需要再次被检查,这是不被推荐的数据处理方式。在开发模式下,Angular 会进行二次检查,如果出现上述情况,二次检查就会报错: ExpressionChangedAfterItHasBeenCheckedError ,在生产环境中,则只会执行一次检查。

并不是在所有的生命周期去调用都会报错,我们把刚才的示例修改一下:

@Component({
  selector: "app-child",
  template: ``, 
})
export class ChildComponent implements OnInit {
  constructor(private parent: ParentComponent) {}

  ngOnInit(): void {
    this.parent.title = "被修改的标题";
  }
}

修改后的代码运行正常,这是为什么呢?这里要说一下Angular检测执行的顺序:

  • 更新所有子子组件绑定的属性

  • 调用所有子组件生命周期的钩子 OnChanges, OnInit, DoCheck ,AfterContentInit

  • 更新当前组件的DOM

  • 调用子组件的变换检测

  • 调用所有子组件的生命周期钩子 ngAfterViewInit

ngAfterViewInit 是在变化检测之后执行的,在执行变化检测后我们更改了父组件的数据,在Angular执行开发模式下的第二次检查时,发现与上一次的值不一致,所以报错,而ngOnInit 的执行在变化检测之前,所以一切正常。

这里提一下AngularJS,AngularJS采用的是双向数据流,错综复杂的数据流使得它不得不多次检查,使得数据最终趋向稳定。理论上,数据可能永远不稳定。AngularJS的策略是,脏检查超过10次,就认为程序有问题,不再进行检查。

变化检测的性能

刚才我们聊了变化检测的工作流程,接下来我想说的是变化检测的性能, 默认情况下,当我们的组件中某个值发生了变化触发了变化检测,那么Angular会从上往下检查所有的组件。 不过Angular对每个组件进行更改检测的速度非常快,因为它可以使用 内联缓存 在几毫秒内执行数千次检查,其中内联缓存可生成对 VM 友好代码。

尽管 Angular 进行了大量优化,但是遇到了大型应用,变化检测的性能仍然会下降,所以我们还需要用一些其他的方式来优化我们的应用。

变化检测的策略

Angular 提供了两种运行变更检测的策略:

  • Default

  • OnPush

Default 策略

默认情况下,Angular 使用 ChangeDetectionStrategy.Default 变更检测策略,每次事件触发变化检测(如用户事件、计时器、XHR、promise 等)时,此默认策略都会从上到下检查组件树中的每个组件。这种对组件的依赖关系不做任何假设的保守检查方式称为 脏检查 ,这种策略在我们应用组件过多时会对我们的应用产生性能的影响。

OnPush 策略

Angular 还提供了一种 OnPush 策略,我们可以修改组件装饰器的 changeDetection 来更改变化检测的策略

@Component({
    selector: 'app-demo',
    // 设置变化检测的策略
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class DemoComponent {
    ...
}

设置为 OnPush 策略后,Angular 每次触发变化检测后会跳过该组件和该组件的所以子组件变化检测

OnPush模式下变化检测流程

在 OnPush 策略下,只有以下这几种情况才会触发组件的变化检测:

  • 输入值(@Input)更改

  • 当前组件或子组件之一触发了事件

  • 手动触发变化检测

  • 使用 async 管道后, observable 值发生了变化

输入值(@Input)更改

在默认的变更检测策略中,Angular 将在 @Input() 数据发生更改或修改时执行变化检测,使用该 OnPush 时,传入 @Input()  的值 必须是一个新的引用 才会触发变化检测。

JavaScript有两种数据类型,值类型和引用类型,值类型包括:number、string、boolean、null、undefined,引用类型包括:Object、Arrary、Function,值类型每次赋值都会分配新的空间,而引用类型比如Object,直接修改属性是引用是不会发生变化的,只有赋一个新的对象才会改变引用。

var a= 1;
var b = a;
b = 2;
console.log(a==b); // false

var obj1 = {a:1};
var obj2 = obj1;
obj2.a = 2;
console.log(obj1); // {a:2}
console.log(obj1 === obj2); //true

obj2= {...obj1};
console.log(obj1 === obj2); //false

当前组件或子组件之一触发了事件

如果 OnPush 组件或其子组件之一触发事件,例如 click,则将触发变化检测(针对组件树中的所有组件)。

需要注意的是在 OnPush 策略中,以下操作不会触发变化检测:

  • setTimeout()

  • setInterval()

  • Promise.resolve().then()

  • this.http.get('...').subscribe()  

手动触发变化检测

有三种手动触发更改检测的方法:

  • **detectChanges(): ** 它会触发当前组件和子组件的变化检测

  • markForCheck(): 它不会触发变化检测,但是会把当前的OnPush组件和所以的父组件为OnPush的组件 ** 标记为需要检测状态** ,在当前或者下一个变化检测周期进行检测

  • ApplicationRef.tick() : 它会根据组件的变化检测策略,触发整个应用程序的更改检测

 可以通过 在线Demo ,更直观的了解这几种触发变化检测的方式

使用 async 管道

内置的 AsyncPipe 订阅一个 observable 并返回它发出的最新值。

每次发出新值时的内部 AsyncPipe 调用 markForCheck

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

减少变化检测次数

刚才我们聊了变化检测的策略,我们可以使用 OnPush 的策略来优化我们的应用,那么这就够了吗? 在我们实际的开发中还会有很多的场景,我们需要通过一些其他的方式来继续优化我们的应用。

场景1:

假如我们在实现一个回车搜索的功能:

@Component({
  selector: "app-enter",
  template: `<input #input type="text" />`,
})
export class EnterComponent implements AfterViewInit {
  @ViewChild("input", { read: ElementRef })
  private inputElementRef: any;

  constructor() {}

  ngAfterViewInit(): void {
    this.inputElementRef.nativeElement.addEventListener(
      "keydown",
      (event: KeyboardEvent) => {
        const keyCode = event.which || event.keyCode;
        if (keyCode === 13) {
          this.search();
        }
      }
    );
  }

  search() {
    // ...
  }
}

大家从上面的示例中可以发现什么问题呢?

我们知道事件会触发Angular的变化检测,在示例中绑定 keydown 事件后,每一次键盘输入都会触发变化检测,而这些变化检测大多数都是多余的检测,只有当按键为 Enter 时,才需要真正的进行变化检测。

在这种情况下,我们就可以利用 NgZone.runOutsideAngular()  来减少变化检测的次数。

@Directive({
    selector: &#39;[enter]&#39;
})
export class ThyEnterDirective implements OnInit {
    @Output() enter = new EventEmitter();

    constructor(private ngZone: NgZone, private elementRef: ElementRef<HTMLElement>) {}

    ngOnInit(): void {
        // 包裹代码将运行在Zone区域之外
        this.ngZone.runOutsideAngular(() => {
            this.elementRef.nativeElement.addEventListener(&#39;keydown&#39;, (event: KeyboardEvent) => {
                const keyCode = event.which || event.keyCode;
                if (keyCode === 13) {
                    this.ngZone.run(() => {
                        this.enter.emit(event);
                    });
                }
            });
        });
    }
}

场景2:

假如我们使用 WebSocket 将大量数据从后端推送到前端,则相应的前端组件应仅每 10 秒更新一次。在这种情况下,我们可以通过调用 detach() 和手动触发它来停用更改检测detectChanges() :

constructor(private cdr: ChangeDetectorRef) {
    cdr.detach(); // 停用变化检测
    setInterval(() => {
      this.cdr.detectChanges(); // 手动触发变化检测
    }, 10 * 1000);
  }

当然使用 ngZone.runOutsideAngular()  也可以处理这种场景。

脱离 Zone.js 开发

之前我们说了Angular 可以自动帮我们进行变化检测,这主要是基于Zone.js来实现,那么很多人潜意识会任务Zone.js 就是 Angular 是一部分,Angular的 应用程序必须基于Zone.js,其实不然,如果我们对应用有极高的性能要求时,我们可以选择移除 Zone.js,移除Zone.js 将会提升应用的性能和打包的体积,不过带来的后果就是我们需要主要去调用变化检测。

如何移除 Zone.js?

手动调用变化检测

在 Ivy 之后,我们有一些新的API可以更方便的调用变化检测

 **ɵmarkDirty: ** 标记一个组件为 dirty 状态 (需要重新渲染) 并将在未来某个时间点安排一个变更检测

ɵdetectChanges: 因为某些效率方面的原因,内部文档不推荐使用  ɵdetectChanges  而推荐使用  ɵmarkDirty , ɵdetectChanges 会触发组件以子组件的变更检测。

移除后的性能

移除Zone.js后变化检测由应用自己来控制,极大的减少了不必要的变化检测次数,同时打包后的提及也减少了 36k

移除前:

 移除后:

测试与变化检测

组件绑定

我们先来看一个组件绑定的例子:

 按我们正常开发组件的想法,当看到这个示例的时候一定认为这个Case是Ok的,但是在运行测试后我们发现这个Case失败了。

在生产环境中,当 Angular 创建一个组件,就会自动进行变更检测。 但是在测试中,**TestBed.createComponent()** 并不会进行变化检测,需要我们手动触发。

修改一下上面的Case:

origin-url0.00KB

origin-url0.00KB

从上面的示例中可以了解到,我们必须通过调用 fixture.detectChanges() 来告诉 TestBed 执行数据绑定。

如果我们在测试中动态改变了绑定值,同样也需要调用 fixture.detectChanges() 。

it("should update title", () => {
    component.title = &#39;Test Title&#39;;
    fixture.detectChanges();
    const h1 = fixture.nativeElement.querySelector("h1");
    expect(h1.textContent).toContain(&#39;Test Title&#39;);
});

自动变更检测

我们发现写测试过程中需要频繁的调用 fixture.detectChanges() ,可能会觉得比较繁琐,那 Angular 可不可以在测试环境中自动运行变化检测呢?

我们可以通过配置  ComponentFixtureAutoDetect  来实现

TestBed.configureTestingModule({
  declarations: [ BannerComponent ],
  providers: [
    { provide: ComponentFixtureAutoDetect, useValue: true }
  ]
});

然后再回头看看刚才的示例:

 上面的示例我们并没有调用 fixture.detectChanges() ,但是测试依然通过了,这是因为我们开启了自动变化检测。

再看一个示例:

위의 예에서는 테스트 코드의 제목 값을 동적으로 수정했는데 테스트가 실패했습니다. 이는 ComponentFixtureAutoDetect가 자동 변경만 수행한다는 사실을 Angular가 알지 못하기 때문입니다. Promise, setTimeout, 클릭 및 기타 DOM 이벤트와 같은 비동기 작업을 감지합니다. 바인딩 값을 수동으로 변경하는 경우에도 fixture.DetectChanges()를 호출하여 변경 감지를 수행해야 합니다. ComponentFixtureAutoDetect  只对异步操作进行自动变化检测,例如 Promise、setTimeout、click 等DOM事件等,如果我们手动更改了绑定值,我们依然还需要调用 fixture.detectChanges()  来执行变化检测。

常见的坑

ngModel

 上面这个示例,绑定值修改后调用了 fixture.detectChanges() , 但是运行测试后仍然报错,这是为什么呢?

 查看Angular源码后我们发现 ** ngModel 的值是通过异步更新的** ,执行fixture.detectChanges() 后虽然触发了变化检测,但是值还并未修改成功。

修改一下测试:

 修改后我们将断言包裹在了 fixture.whenStable()  中,然后测试通过,那 whenStable()  是什么呢?

whenStable(): Promise : 当夹具稳定时解析的承诺 当事件已触发异步活动或异步变更检测后,可用此方法继续执行测试。

当然除了用 fixture.whenStable()  我们也可以用 tick()  来解决这个问题

tick() :为 fakeAsync Zone 中的计时器模拟异步时间流逝 在此函数开始时以及执行任何计时器回调之后,微任务队列就会耗尽

测试 OnPush 组件

 上面这个示例,我们在修改属性后调用了 fixture.detectChanges()  ,但是测试未通过,这是为什么呢?我们发现这个示例与第一个示例唯一的区别就是这个组件是一个 OnPush 组件,之前我们说过默认变化检测会跳过 OnPush 组件的,只有在特定的几种情况下才会触发变化检测的,遇到这种情况如何解决呢?

 我们可以手动获取组件的 ChangeDetectorRef

일반적인 함정

ngModel

위의 예에서는 바인딩 값을 수정한 후 fixture.DetectChanges()를 호출했지만 여전히 오류가 발생합니다. 테스트를 실행한 후 보고되었는데, 이유는 무엇입니까?

Angular 소스 코드를 본 후 우리는 다음을 발견했습니다. ** ngModel 값은 비동기적으로 업데이트됩니다** fixture.DetectChanges() 실행 후 변경 감지가 실행되지만 값이 성공적으로 수정되지 않았습니다. 테스트 수정:

수정 후 어설션은 fixture.whenStable()로 래핑되고 테스트가 통과됩니다. 그렇다면 whenStable()은 무엇인가요?

whenStable(): Promise: 설비가 안정적일 때 해결되는 약속입니다. 이 메서드는 이벤트가 비동기 활동 또는 비동기 변경 감지를 트리거한 후에도 테스트를 계속 실행하는 데 사용할 수 있습니다.
  • 물론 fixture.whenStable()을 사용하는 것 외에도 tick()을 사용하여 이 문제를 해결할 수도 있습니다

  • tick(): 처음에는 fakeAsync Zone의 타이머에 대한 비동기 시간 경과를 시뮬레이션하고 이 함수 실행 타이머 콜백 후에 마이크로태스크 대기열이 소진됩니다
  • OnPush 구성 요소 테스트

위 예에서는 fixture.DetectChanges() code>를 호출했습니다. , 그런데 테스트가 실패했습니다. 이유는 무엇입니까? 이 예제와 첫 번째 예제의 유일한 차이점은 이 구성 요소가 <code>OnPush 구성 요소라는 것입니다. 이전에는 기본 변경 감지가 OnPush 구성 요소를 건너뛴다고 말했습니다. , 특정 상황에서 변경 감지가 트리거되는 경우에만 이 상황을 해결하는 방법은 무엇입니까?

구성 요소의 를 수동으로 얻을 수 있습니다. ChangeDetectorRef

를 사용하여 변경 감지를 적극적으로 트리거합니다.
    Extensions
  • Virtual DOM vs. Incremental DOM
  • Angular Ivy는 증분 DOM을 사용하기 때문에 주류 프레임워크에서 본 것과는 매우 다른 새로운 Angular 렌더러입니다. 증분 DOM이란 무엇입니까? 가상 돔과 어떻게 다릅니까?

  • Virtual DOM

  • 먼저 가상 DOM에 대해 이야기해 봅시다. 브라우저에서 DOM을 직접 조작하는 것은 성능 집약적이며, 가상 DOM의 주요 개념은 메모리에 있는 UI의 가상 표현, Diff 작업을 통해 현재 메모리와 메모리에 있는 마지막 뷰의 차이를 비교함으로써 불필요한 Dom 작업을 줄이고 다른 Dom만 변경합니다.

  • Virtual DOM 실행 프로세스:

UI가 변경되면 전체 UI가 Virtual DOM으로 렌더링됩니다.

이전 가상 DOM 표현과 현재 가상 DOM 표현의 차이를 계산하세요.

변경사항으로 실제 DOM을 업데이트하세요. 🎜🎜🎜🎜🎜🎜🎜🎜가상 DOM의 장점: 🎜🎜🎜🎜🎜효율적인 Diff 알고리즘. 🎜🎜🎜🎜간단하고 성능 향상에 도움이 됩니다. 🎜🎜🎜🎜 React 없이 작동 🎜🎜🎜🎜 충분히 가벼움 🎜🎜🎜🎜 상태 전환을 고려하지 않고 애플리케이션 구축 가능 🎜🎜🎜🎜🎜Incremental DOM🎜🎜🎜Incremental Dom의 주요 개념은 구성 요소를 일련의 명령으로 컴파일하는 것입니다. DOM 트리를 생성하고 데이터가 변경되면 해당 위치에서 업데이트합니다. 🎜🎜🎜🎜🎜 예: 🎜
@Component({
  selector: &#39;todos-cmp&#39;,
  template: `
    <p *ngFor="let t of todos|async">
        {{t.description}}
    </p>
  `
})
class TodosComponent {
  todos: Observable<Todo[]> = this.store.pipe(select(&#39;todos&#39;));
  constructor(private store: Store<AppState>) {}
}

编译后:

var TodosComponent = /** @class */ (function () {
  function TodosComponent(store) {
    this.store = store;
    this.todos = this.store.pipe(select(&#39;todos&#39;));
  }

  TodosComponent.ngComponentDef = defineComponent({
    type: TodosComponent,
    selectors: [["todos-cmp"]],
    factory: function TodosComponent_Factory(t) {
      return new (t || TodosComponent)(directiveInject(Store));
    },
    consts: 2,
    vars: 3,
    template: function TodosComponent_Template(rf, ctx) {
      if (rf & 1) { // create dom
        pipe(1, "async");
        template(0, TodosComponent_p_Template_0, 2, 1, null, _c0);
      } if (rf & 2) { // update dom
        elementProperty(0, "ngForOf", bind(pipeBind1(1, 1, ctx.todos)));
      }
    },
    encapsulation: 2
  });

  return TodosComponent;
}());

增量DOM的优点:

  • 渲染引擎可以被Tree Shakable,降低编译后的体积

  • 占用较低的内存

为什么可渲染引擎可以被 Tree Shakable?

Tree Shaking 是指在编译目标代码时移除上下文中未引用的代码 ,增量 DOM 充分利用了这一点,因为它使用了基于指令的方法。正如示例所示,增量 DOM 在编译之前将每个组件编译成一组指令,这有助于识别未使用的指令。在 Tree Shakable 过程中,可以将这些未使用的的指令删除掉。

减少内存的使用

与虚拟 DOM 不同,增量 DOM 在重新呈现应用程序 UI 时不会生成真实 DOM 的副本。此外,如果应用程序 UI 没有变化,增量 DOM 就不会分配任何内存。大多数情况下,我们都是在没有任何重大修改的情况下重新呈现应用程序 UI。因此,按照这种方法可以极大的减少设备内存使用。

总结

至此,Angular 变化检测相关的内容就介绍完了,这是我在公司内部 2个小时的分享内容,在准备的过程中参考了很多优秀的资料,自己也学习到了更深层,更细节的一些技术点。如果大家有不理解的,欢迎在评论区沟通,如果有需要改正的地方,也欢迎大家指出,希望这篇文章可以帮助大家更好的理解Angular的变化检测。

更多编程相关知识,请访问:编程学习!!

위 내용은 Angular의 변경 감지에 대한 심층적인 대화를 안내해 드리겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 csdn.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제