Hỏi - đáp Nơi cung cấp thông tin nghề nghiệp và giải đáp những thắc mắc thường gặp của bạn

Cách cải thiện hiệu suất của ứng dụng Angular với tính năng Phát hiện thay đổi và NgZone

Phát hiện thay đổi

Chúng ta thường nghe nói về việc sử dụng framework như là “Framework sẽ làm mọi thứ cho bạn - nhanh hơn và hiệu quả hơn. Bạn không cần phải suy nghĩ về bất cứ điều gì. Chỉ cần quản lý dữ liệu. ” Có lẽ điều này đúng với một ứng dụng rất đơn giản. Nhưng nếu bạn phải làm việc với thông tin đầu vào của người dùng và liên tục hoạt động trên dữ liệu thì việc biết quá trình phát hiện và kết xuất thay đổi hoạt động như thế nào là điều bắt buộc.

Change Detection là một cơ chế chịu trách nhiệm kiểm tra các thay đổi trong Angular. Là kết quả của các hoạt động khác nhau như thay đổi giá trị của thuộc tính lớp, hoàn thành hoạt động không đồng bộ, phản hồi của yêu cầu HTTP, bắt đầu một trong các sự kiện trình duyệt (ví dụ: sự kiện 'nhấp chuột'), v.v. - quá trình phát hiện thay đổi bắt đầu từ gốc của cây thành phần của ứng dụng.

Vì mục tiêu chính của quy trình là hiểu cách kết xuất lại một thành phần, điều cần thiết là phải xác minh dữ liệu được sử dụng trong các mẫu: nếu nó đã thay đổi thì bản thân mẫu được đánh dấu là “đã thay đổi” và sẽ được hiển thị lại .

Chúng ta có thể thấy điều này bằng một ví dụ đơn giản. Mỗi khi chúng ta nhấp vào nút, mã của thành phần chỉ cập nhật 'số lượng' thuộc tính của thành phần. Tuy nhiên, HTML được cập nhật tự động.

import { Component } from '@angular/core';
@Component({
    selector: 'app-root',
    template: `
        {{runChangeDetection}}
        <div>{{count}}</div>
        <button (click)="handler()"> ClickMe </button>
    `
})
export class AppComponent {
 
count = 0;
get runChangeDetection() {
       console.log('Checking the view');
       return true;
     }
handler() {
       this.count++;
       // user does not need to trigger change detection manually
    }
}

Hơn nữa, tính năng phát hiện thay đổi sẽ được kích hoạt ngay cả khi nút không làm gì cả:

handler() {
       
    }
<html>
<div id="dataDiv"></div>
<button id="btn" (click)="handler()"> ClickMe </button>
<script>
let count = 0;
// initial rendering
detectChange();
function renderHTML() {
document.getElementById('dataDiv').innerText = count;
}
function detectChange() {
const currentValue = document.getElementById('dataDiv').innerText;
if (currentValue !== count) {
renderHTML();
}
}
// update data inside button click event handler
document.getElementById('btn').addEventListener('click', () => {
// update count
count ++;
// call detectChange manually
detectChange();
});
</script>
</html>

Sau khi chúng tôi cập nhật dữ liệu, chúng tôi cần gọi exploreChange () theo cách thủ công để kiểm tra xem dữ liệu đã thay đổi hay chưa. Nếu dữ liệu đã thay đổi, chúng tôi kết xuất lại HTML để phản ánh dữ liệu đã cập nhật.

Trong Angular, các bước này là không cần thiết. Bất cứ khi nào bạn cập nhật dữ liệu, HTML của bạn sẽ được cập nhật tự động.

Một ví dụ tương tự về kích hoạt phát hiện thay đổi có thể được nhìn thấy trong ví dụ về bộ đếm thời gian trong đó cứ sau 100ms giá trị của bộ đếm sẽ được thay đổi, vì bộ phát hiện thay đổi kết quả sẽ kiểm tra dữ liệu trong mẫu sau mỗi 100ms (điều này ảnh hưởng xấu đến hiệu suất, đặc biệt là trong ứng dụng lớn, vì phát hiện thay đổi được kích hoạt trong một thành phần sẽ kích hoạt phát hiện thay đổi trong tất cả các thành phần).

setInterval( () => this.count++,100)

Zone.js

Hiểu cách Angular theo dõi các thuộc tính của lớp và các hoạt động đồng bộ khá đơn giản. Phần phức tạp là các hoạt động không đồng bộ. Ý tưởng cơ bản của Zone.js là vùng. Bản thân khu vực là “ bối cảnh thực hiện ”, địa điểm và trạng thái của sự kiện. Sau khi hoàn tất hoạt động không đồng bộ, hàm gọi lại được thực thi trong cùng một vùng mà nó đã được đăng ký. Vì vậy, Angular tìm ra nơi thay đổi xảy ra và những gì cần kiểm tra.

Các API bản vá của Zone.js Monkey

Theo định nghĩa, bản vá Monkey về cơ bản ghi đè API gốc. Bằng cách này Zone.js ghi đè hầu như tất cả các hàm và phương thức không đồng bộ nguyên bản. Do đó, nó có thể theo dõi khi nào lệnh gọi lại của hàm không đồng bộ được thực thi. Đó là, khu vực cho Angular biết khi nào và ở đâu để bắt đầu quá trình phát hiện thay đổi.

// this is the new version of addEventListener
function addEventListener(eventName, callback) {
     // call the real addEventListener
     callRealAddEventListener(eventName, function() {
        // first call the original callback
        callback(...);     
        // and then run Angular-specific functionality
        let changed = angular.runChangeDetection();
         if (changed) {
             angular.reRenderUIPart();
         }
     });
}

Nhược điểm

Zone.js thay đổi hành vi tiêu chuẩn của API trình duyệt. Mặc dù thực tế là Monkey Patch được thực hiện gọn gàng, nhưng sẽ có thêm chi phí về thời gian khi gọi các hàm cơ sở. Mặc dù những chi phí này là nhỏ, nhưng chúng rất đáng chú ý đối với các ứng dụng lớn.

NgZone run()runOutsideOfAngular()

Từ tài liệu chính thức của Angular: “ Trong khi Zone.js có thể giám sát tất cả các trạng thái của hoạt động đồng bộ và không đồng bộ, Angular cũng cung cấp thêm một dịch vụ gọi là NgZone. Dịch vụ này tạo một khu vực được đặt tên angularđể tự động kích hoạt phát hiện thay đổi. ”

NgZone.run ()

Zone xử lý hầu hết các API không đồng bộ như setTimeout (), Promise.then () và addEventListenner. Do đó, bạn không cần phải kích hoạt phát hiện thay đổi cho chúng theo cách thủ công.

Vẫn còn một số API của bên thứ ba mà Zone không xử lý. Trong những trường hợp đó, dịch vụ NgZone cung cấp phương thức run () cho phép bạn thực thi một hàm bên trong vùng góc.

export class AppComponent implements OnInit {
constructor(private ngZone: NgZone) {}
ngOnInit() {
// New async API is not handled by Zone, so you need to
// use ngZone.run() to make the asynchronous operation in the angular zone
// and trigger change detection automatically.
this.ngZone.run(() => {
someNewAsyncAPI(() => {
// update the data of the component
});
});
}
}
view rawngZonerun.ts hosted with ❤ by GitHub

Chức năng này và tất cả các hoạt động không đồng bộ trong chức năng đó, sẽ tự động kích hoạt phát hiện thay đổi vào đúng thời điểm.

runOutsideAngular

Một trường hợp phổ biến khác là khi bạn không muốn kích hoạt tính năng phát hiện thay đổi. Điều này đạt được bằng cách sử dụng một phương thức NgZone khác: runOutsideAngular ().

Giả sử chúng ta có một hoạt ảnh trong ứng dụng, tức là phần tử <div> đơn giản thay đổi nền của nó cứ sau 50 ms bằng cách sử dụng setInterval đơn giản.

import { Component, OnInit, ViewChild, ElementRef, NgZone, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-animated-div',
template: `<div #div></div>`,
styleUrls: ['./animated-div.component.scss']
})
export class AnimatedDivComponent implements OnInit {
@ViewChild('div', { static: true }) div: ElementRef
constructor(private ngZone: NgZone) { }
ngOnInit(): void {
this.changeColor();
}
private setRandomColor() {
return ['red', 'orange', 'yellow', 'green', 'blue', 'purple'][Math.random() * 6 | 0];
}
private changeColor() {
setInterval(_ => this.div.nativeElement.style.background = this.setRandomColor(), 50)
}
}
view rawanimation.ts hosted with ❤ by GitHub

Hãy tưởng tượng điều gì sẽ xảy ra nếu ứng dụng của bạn có hơn 1000 thành phần. Trong tất cả chúng, phát hiện thay đổi sẽ được kích hoạt sau mỗi 50ms, điều này sẽ ảnh hưởng đáng kể đến hiệu suất .

Tuy nhiên, như bạn có thể giả định, có một cách đơn giản để khắc phục nó. Bạn chỉ có thể bọc nó bằng NgZone.runOutsideAngular như thế này:

ngOnInit(): void {
   this.ngZone.runOutsideAngular( _ => {
   this.changeColor()
   })
}

Thay đổi chiến lược phát hiện

Angular có hai biến thể của các chiến lược phát hiện thay đổi:

  • Mặc định
  • OnPush
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss'], 
    //changeDetection: ChangeDetectionStrategy.OnPush
})

Chúng ta hãy xem hình dung của chiến lược Mặc định phát hiện thay đổi. Giả sử trình khởi tạo quá trình phát hiện thay đổi trong cây thành phần là thành phần “1” (một sự kiện nhấp chuột đã được kích hoạt).

Chiến lược phát hiện thay đổi OnPush

Chiến lược phát hiện thay đổi OnPush cung cấp khả năng bỏ qua các kiểm tra không cần thiết đối với thành phần và tất cả các thành phần con của nó. Nếu chúng tôi thay đổi chiến lược cho thành phần “A” thành OnPush và kích hoạt phát hiện thay đổi từ thành phần “1”, thì tính năng phát hiện thay đổi sẽ không hoạt động đối với các thành phần “A”, “B” và “C”.

GIF tiếp theo thể hiện việc bỏ qua các phần của cây thành phần bằng cách sử dụng chiến lược phát hiện thay đổi OnPush:

Bạn có thể kiểm tra các biến thể khác nhau trên ví dụ này tại đây .

Khi sử dụng chiến lược OnPush, Angular sẽ cập nhật thành phần nếu:

  1. Tham chiếu đầu vào đã thay đổi
@Component({
selector: 'child',
template: `
<h1>{{object.name}}</h1>
{{runChangeDetection}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent {
@Input() object;
get runChangeDetection() {
console.log('Checking the view');
return true;
}
}
@Component({
selector: 'app-root',
template: `
<child [object]="object"></child>
<button (click)="onClick()">Click</button>
`
})
export class AppComponent {
object = {
name: 'Angular'
};
onClick() {
this.object.name = 'React';
}
}
view rawinput.ts hosted with ❤ by GitHub
/** Returns false in our case */
 if( oldValue !== newValue ) { 
   runChangeDetection();
 }
onClick() {
this.object = {
name: 'React'
}
view rawinputChanges.ts hosted with ❤ by GitHub

2. Một sự kiện bên trong một thành phần hoặc con cháu của nó

Thành phần có trạng thái bên trong, trạng thái này được cập nhật khi một sự kiện xảy ra từ chính thành phần đó hoặc con cháu của nó.

Ví dụ:

@Component({
template: `
<button (click)="increase()">Click</button>
{{count}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
increase() {
this.count++;
}
}
view rawclick.ts hosted with ❤ by GitHub

Khi chúng tôi nhấp vào nút, Angular bắt đầu chu kỳ phát hiện thay đổi và chế độ xem được cập nhật như mong đợi.

Có lẽ bạn nghĩ rằng tất cả các hoạt động không đồng bộ sẽ kích hoạt cơ chế phát hiện thay đổi, như đã đề cập ở phần đầu. Nhưng đối với chiến lược OnPush, quy tắc chỉ áp dụng cho các sự kiện DOM.

Ví dụ

@Component({
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
constructor() {
setTimeout(() => this.count ++, 0);
setInterval(() => this.count ++, 100);
Promise.resolve().then(() => this.count = 10);
this.http.get('https://count.com').subscribe(res => {
this.count = res;
});
}
}
view rawatherEvents.ts hosted with ❤ by GitHub

Lưu ý rằng thuộc tính count đã thay đổi, nhưng sẽ không có thay đổi nào trong HTML, chúng tôi nên chạy phát hiện thay đổi theo cách thủ công.

3. Phát hiện thay đổi được kích hoạt theo cách thủ công

Angular cung cấp ba phương pháp để khởi chạy thủ công cơ chế phát hiện các thay đổi.

  • DetChanges () yêu cầu Angular bắt đầu phát hiện các thay đổi trong thành phần và các thành phần con của nó (nếu chúng có chiến lược mặc định). Ví dụ trước có thể được viết lại như sau (bây giờ phát hiện thay đổi sẽ được kích hoạt)
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
constructor(private cd: ChangeDetectorRef) {
setTimeout(() => {
this.count++
this.cd.detectChanges()
}, 0);
this.http.get('https://count.com').subscribe(res => {
this.count = res;
this.cd.detectChanges()
});
}
}
view rawdatectChanges.ts hosted with ❤ by GitHub

ApplicationRef.tick () giống như zone.run () phát hiện thay đổi trên toàn bộ ứng dụng (ngoại lệ là các nhánh của cây với chiến lược OnPush) markForCheck () không kích hoạt phát hiện thay đổi. Thay vào đó, nó đánh dấu thành phần và tất cả cha mẹ của nó (bất kể chiến lược của chúng được đặt là gì) mà chúng nên được kiểm tra trong chu kỳ phát hiện thay đổi hiện tại hoặc tiếp theo.

Đường ống không đồng bộ đăng ký đối tượng hoặc lời hứa được quan sát và trả về giá trị cuối cùng cho nó.

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, } from 'rxjs/operators';
@Component({
selector: 'app-b',
template: `<img [src]="user$|async">`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent implements OnInit {
user$: Observable<string>
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.user$ = this.http.get('https://randomuser.me/api/').pipe(map((res:any):string => {
return res = res.results[0].picture.medium
}))
}
}
view rawasyncPipe.ts hosted with ❤ by GitHub

Nội bộ các AsyncPipecuộc gọi markForCheck()mỗi khi một giá trị mới được phát ra

Phần kết luận

Trong hầu hết các trường hợp, đặc biệt nếu ứng dụng của bạn không vượt quá 100 thành phần, chiến lược mặc định phát hiện thay đổi của Angular hoạt động khá nhanh. Nhưng trong các ứng dụng lớn, đặc biệt nếu mục tiêu của bạn là hiệu suất, bạn cần tự mình thực hiện công việc này và phát triển thuật toán phát hiện thay đổi phù hợp với trường hợp cụ thể của bạn. Tuy nhiên, điều quan trọng là phải biết cách Angular hoạt động dưới mui xe để tránh hoặc sửa các lỗi khác nhau.

Bạn có thể thực hành các biến thể khác nhau của phát hiện thay đổi trong ví dụ được mô tả trong bài viết này tại đây .