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)
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.
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();
}
});
}
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.
run()
và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. ”
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 | |
}); | |
}); | |
} | |
} |
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.
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) | |
} | |
} |
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()
})
}
Angular có hai biến thể của các chiến lược phát hiện thay đổi:
@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 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:
@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'; | |
} | |
} |
/** Returns false in our case */
if( oldValue !== newValue ) {
runChangeDetection();
}
onClick() { | |
this.object = { | |
name: 'React' | |
} |
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++; | |
} | |
} |
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; | |
}); | |
} | |
} |
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.
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() | |
}); | |
} | |
} |
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 | |
})) | |
} | |
} |
Nội bộ các AsyncPipe
cuộc gọi markForCheck()
mỗi khi một giá trị mới được phát ra
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 .