Triển khai tính năng phát hiện thay đổi nhanh hơn để mang lại trải nghiệm tốt hơn cho người dùng.
Angular chạy cơ chế phát hiện thay đổi theo định kỳ để các thay đổi đối với mô hình dữ liệu được phản ánh trong khung hiển thị của ứng dụng. Bạn có thể kích hoạt tính năng phát hiện thay đổi theo cách thủ công hoặc thông qua một sự kiện không đồng bộ (ví dụ: một lượt tương tác của người dùng hoặc một lượt hoàn tất XHR).
Tính năng phát hiện thay đổi là một công cụ mạnh mẽ, nhưng nếu chạy quá thường xuyên, tính năng này có thể kích hoạt nhiều phép tính và chặn luồng chính của trình duyệt.
Trong bài đăng này, bạn sẽ tìm hiểu cách kiểm soát và tối ưu hoá cơ chế phát hiện thay đổi bằng cách bỏ qua các phần của ứng dụng và chỉ chạy tính năng phát hiện thay đổi khi cần thiết.
Bên trong cơ chế phát hiện thay đổi của Angular
Để hiểu cách hoạt động của tính năng phát hiện thay đổi của Angular, hãy xem một ứng dụng mẫu!
Bạn có thể tìm thấy mã của ứng dụng trong kho lưu trữ GitHub này.
Ứng dụng này liệt kê nhân viên của 2 phòng ban trong một công ty (bán hàng và Nghiên cứu và phát triển) và có 2 thành phần:
AppComponent
là thành phần gốc của ứng dụng và- Hai trường hợp
EmployeeListComponent
, một cho hoạt động bán hàng và một cho hoạt động nghiên cứu và phát triển.
Bạn có thể thấy 2 thực thể EmployeeListComponent
trong mẫu cho AppComponent
:
<app-employee-list
[data]="salesList"
department="Sales"
(add)="add(salesList, $event)"
(remove)="remove(salesList, $event)"
></app-employee-list>
<app-employee-list
[data]="rndList"
department="R&D"
(add)="add(rndList, $event)"
(remove)="remove(rndList, $event)"
></app-employee-list>
Đối với mỗi nhân viên, sẽ có một tên và một giá trị bằng số. Ứng dụng này truyền giá trị bằng số của nhân viên đến một phép tính kinh doanh và trực quan hoá kết quả trên màn hình.
Bây giờ, hãy xem EmployeeListComponent
:
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Component(...)
export class EmployeeListComponent {
@Input() data: EmployeeData[];
@Input() department: string;
@Output() remove = new EventEmitter<EmployeeData>();
@Output() add = new EventEmitter<string>();
label: string;
handleKey(event: any) {
if (event.keyCode === 13) {
this.add.emit(this.label);
this.label = '';
}
}
calculate(num: number) {
return fibonacci(num);
}
}
EmployeeListComponent
chấp nhận danh sách nhân viên và tên phòng ban làm dữ liệu đầu vào. Khi người dùng cố gắng xoá hoặc thêm nhân viên, thành phần này sẽ kích hoạt một đầu ra tương ứng. Thành phần này cũng xác định phương thức calculate
, giúp triển khai phép tính nghiệp vụ.
Sau đây là mẫu cho EmployeeListComponent
:
<h1 title="Department">{{ department }}</h1>
<mat-form-field>
<input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
<mat-list-item *ngFor="let item of data">
<h3 matLine title="Name">
{{ item.label }}
</h3>
<md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
{{ calculate(item.num) }}
</md-chip>
</mat-list-item>
</mat-list>
Đoạn mã này lặp lại tất cả nhân viên trong danh sách và hiển thị một mục danh sách cho từng nhân viên. Nó cũng bao gồm một chỉ thị ngModel
để liên kết dữ liệu hai chiều giữa đầu vào và thuộc tính label
được khai báo trong EmployeeListComponent
.
Với 2 thực thể EmployeeListComponent
, ứng dụng sẽ tạo thành cây thành phần sau:
AppComponent
là thành phần gốc của ứng dụng. Các thành phần con của nó là 2 thực thể của EmployeeListComponent
. Mỗi thực thể có một danh sách các mục (E1, E2, v.v.) đại diện cho từng nhân viên trong bộ phận.
Khi người dùng bắt đầu nhập tên của một nhân viên mới vào hộp nhập trong EmployeeListComponent
, Angular sẽ kích hoạt tính năng phát hiện thay đổi cho toàn bộ cây thành phần bắt đầu từ AppComponent
. Điều này có nghĩa là trong khi người dùng nhập vào dữ liệu đầu vào văn bản, Angular sẽ liên tục tính toán lại các giá trị số được liên kết với từng nhân viên để xác minh rằng các giá trị đó không thay đổi kể từ lần kiểm tra gần đây nhất.
Để xem tốc độ chậm đến mức nào, hãy mở phiên bản chưa được tối ưu hoá của dự án trên StackBlitz rồi thử nhập tên nhân viên.
Bạn có thể xác minh rằng tình trạng chậm là do hàm fibonacci
gây ra bằng cách thiết lập dự án mẫu và mở thẻ Hiệu suất của Công cụ cho nhà phát triển của Chrome.
- Nhấn tổ hợp phím `Control+Shift+J` (hoặc `Command+Option+J` trên máy Mac) để mở DevTools.
- Nhấp vào thẻ Hiệu suất.
Bây giờ, hãy nhấp vào biểu tượng Ghi (ở góc trên cùng bên trái của bảng điều khiển Hiệu suất) rồi bắt đầu nhập vào một trong các hộp văn bản trong ứng dụng. Sau vài giây, hãy nhấp lại vào biểu tượng Ghi để dừng ghi. Sau khi Chrome DevTools xử lý tất cả dữ liệu lập hồ sơ mà công cụ này thu thập được, bạn sẽ thấy nội dung như sau:
Nếu có nhiều nhân viên trong danh sách, quy trình này có thể chặn luồng giao diện người dùng của trình duyệt và gây ra hiện tượng giảm khung hình, dẫn đến trải nghiệm người dùng kém.
Bỏ qua các cây con thành phần
Khi người dùng nhập vào đầu vào văn bản cho sales EmployeeListComponent
, bạn biết rằng dữ liệu trong bộ phận R&D không thay đổi, vì vậy, không có lý do gì để chạy tính năng phát hiện thay đổi trên thành phần này. Để đảm bảo phiên bản R&D không kích hoạt tính năng phát hiện thay đổi, hãy đặt changeDetectionStrategy
của EmployeeListComponent
thành OnPush
:
import { ChangeDetectionStrategy, ... } from '@angular/core';
@Component({
selector: 'app-employee-list',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}
Giờ đây, khi người dùng nhập vào một đầu vào văn bản, tính năng phát hiện thay đổi sẽ chỉ được kích hoạt cho bộ phận tương ứng:
Bạn có thể tìm thấy quy trình tối ưu hoá này được áp dụng cho ứng dụng gốc tại đây.
Bạn có thể đọc thêm về chiến lược phát hiện thay đổi OnPush
trong tài liệu chính thức của Angular.
Để xem hiệu quả của việc tối ưu hoá này, hãy nhập một nhân viên mới vào ứng dụng trên StackBlitz.
Sử dụng pure pipe
Mặc dù chiến lược phát hiện thay đổi cho EmployeeListComponent
hiện được đặt thành OnPush
, nhưng Angular vẫn tính toán lại giá trị số cho tất cả nhân viên trong một phòng ban khi người dùng nhập vào đầu vào văn bản tương ứng.
Để cải thiện hành vi này, bạn có thể tận dụng các đường ống thuần tuý. Cả pipe thuần tuý và pipe không thuần tuý đều chấp nhận dữ liệu đầu vào và trả về kết quả có thể dùng trong một mẫu. Điểm khác biệt giữa hai loại này là pure pipe sẽ chỉ tính toán lại kết quả nếu nhận được một đầu vào khác so với lần gọi trước đó.
Hãy nhớ rằng ứng dụng này tính toán một giá trị để hiển thị dựa trên giá trị bằng số của nhân viên, gọi phương thức calculate
được xác định trong EmployeeListComponent
. Nếu bạn di chuyển phép tính sang một pipe thuần tuý, Angular sẽ chỉ tính toán lại biểu thức pipe khi các đối số của biểu thức đó thay đổi. Khung sẽ xác định xem các đối số của đường ống có thay đổi hay không bằng cách thực hiện kiểm tra tham chiếu. Điều này có nghĩa là Angular sẽ không thực hiện bất kỳ phép tính lại nào trừ phi giá trị số cho một nhân viên được cập nhật.
Sau đây là cách di chuyển phép tính kinh doanh sang một đường ống có tên là CalculatePipe
:
import { Pipe, PipeTransform } from '@angular/core';
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Pipe({
name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
transform(val: number) {
return fibonacci(val);
}
}
Phương thức transform
của đường ống sẽ gọi hàm fibonacci
. Lưu ý rằng đường ống này là đường ống thuần tuý. Angular sẽ coi tất cả các pipe là thuần tuý, trừ phi bạn chỉ định khác.
Cuối cùng, hãy cập nhật biểu thức bên trong mẫu cho EmployeeListComponent
:
<mat-chip-list>
<md-chip>
{{ item.num | calculate }}
</md-chip>
</mat-chip-list>
Vậy là xong! Giờ đây, khi người dùng nhập vào dữ liệu đầu vào văn bản được liên kết với bất kỳ phòng ban nào, ứng dụng sẽ không tính toán lại giá trị bằng số cho từng nhân viên.
Trong ứng dụng bên dưới, bạn có thể thấy thao tác nhập liệu mượt mà hơn rất nhiều!
Để xem hiệu quả của lần tối ưu hoá gần đây nhất, hãy thử ví dụ này trên StackBlitz.
Bạn có thể xem mã có tính năng tối ưu hoá pure pipe của ứng dụng gốc tại đây.
Kết luận
Khi gặp phải tình trạng chậm trong thời gian chạy trong một ứng dụng Angular:
- Hồ sơ ứng dụng bằng Công cụ cho nhà phát triển của Chrome để xem nguyên nhân gây ra tình trạng chậm.
- Giới thiệu chiến lược phát hiện thay đổi
OnPush
để cắt tỉa các cây con của một thành phần. - Di chuyển các phép tính phức tạp sang các đường ống thuần tuý để cho phép khung thực hiện việc lưu vào bộ nhớ đệm các giá trị đã tính toán.