Lạm bàn về Angular Life Cycle và Change Detection

Mở bài

Như chúng ta có thể đã biết, Angular được xây dựng dựa trên các component và directive, trong đó bản thân component cũng là một directive đặc biệt có kèm thêm HTML template và CSS. Quá trình Angular tạo ra các component/directive cho đến khi kết thúc cuộc đời của chúng được gọi là một “vòng đời” – life cycle. Trong vòng đời ấy, Angular thêm vào các function gọi là “life cycle hooks” để tương tác với mỗi giai đoạn của cuộc đời component. Bài viết này sẽ mô tả cách mà các hàm này được gọi lúc component khởi tạo và thông qua cơ chế Change Detection.

Các hàm life cycle hook

Giả sử ta tạo một component rỗng, tức là không có content nào trong nó, và thử log lại các hàm life cycle hook thì khi khởi chạy sẽ có kết quả sau:

  1. ngOnInit
  2. ngDoCheck
  3. ngAfterContentInit
  4. ngAfterContentChecked
  5. ngAfterViewInit
  6. ngAfterViewChecked

Tất nhiên là ta tạm bỏ qua hàm constructor và hàm ngOnDestroy được gọi lần lượt khi khởi tạo và huỷ bỏ component. Như vậy có một hàm không được gọi đến đó là ngOnChanges, bởi hàm này chỉ được gọi khi có thay đổi về giá trị Input truyền vào component. Có thể chứng minh bằng cách thêm vào một Input cho component và truyền vào cho nó một giá trị

// hello.component.ts
...
@Input() name!: string;
...
// app.component.html
...
<hello [name]="Jenny"></hello>
...

khi đó hàm ngOnChanges sẽ chạy trước hàm ngOnInit và ngay sau hàm constructor:

  1. constructor
  2. ngOnChanges
  3. ngOnInit
  4. ngDoCheck
  5. ngAfterContentInit
  6. ngAfterContentChecked
  7. ngAfterViewInit
  8. ngAfterViewChecked
  9. ngOnDestroy

Như vậy component đã chạy đủ tất cả các hàm hook trong life cycle, ở đây có 8 hàm tất cả là các hàm bắt đầu bằng tiền tố ng. Vậy sau khi component đã khởi tạo xong các hàm này còn chạy lần nào nữa không?

Good question!!! Trong Angular có một cơ chế gọi là Change Detection, tức phát hiện sự thay đổi, sẽ chạy một số hàm tương ứng trong số hàm life cycle hook ở trên. Để biết hàm nào sẽ được khởi chạy, ta tạo một button click để thay đổi một giá trị số đếm, mỗi lần tăng 1; khi đó ta sẽ có 3 hàm được khởi chạy theo thứ tự như sau:

  1. ngDoCheck
  2. ngAfterContentChecked
  3. ngAfterViewChecked
hello.component.ts

Nếu như có thay đổi từ giá trị Input (cụ thể là name trong ví dụ trên) của component thì sẽ run thêm hàm ngOnChanges

  1. ngOnChanges
  2. ngDoCheck
  3. ngAfterContentChecked
  4. ngAfterViewChecked

Ta đã biết hàm ngOnChanges có liên hệ với sự thay đổi đối với data đầu vào của component, vậy còn các hàm còn lại thì sao?

Ở đây hàm ngDoCheck là hàm Angular “để dành” cho dev chúng ta có thể “vọc” và tạo ra cơ chế change detection riêng, chẳng hạn những thay đổi mà bản thân Angular không biết được.

Hàm ngAfterContentChecked liên quan đến phần code HTML được chèn vào component từ bên ngoài thông qua một kỹ thuật gọi là “content projection”.

Hàm ngAfterViewChecked dùng để xử lý đối với các thay đổi trong child view của component, trong đó ta dùng @ViewChild để query tới phần HTML này.

Lưu ý là 4 hàm ở trên được gọi rất thường xuyên mỗi khi Angular chạy Change Detection, vì vậy để tránh ảnh hưởng tới performance của ứng dụng ta cần viết chúng một cách gọn gẽ nhất có thể.

Change Detection – behind the scene!

Ở trên chúng ta đã thấy có hai trường hợp mà Angular gọi đến Change Detection:

  1. Khi component/directive khởi tạo, được Angular gọi trực tiếp change detection để render ra UI thông qua hàm ApplicationRef.tick().
  2. Khi có các event listener được khởi chạy (hàm button onClick ở ví dụ trên), lúc đó event listener trong DOM sẽ update data trong component và kích hoạt change detection.

Ngoài ra, còn có các trường hợp khác:

  1. Khi request data (ví dụ lấy data từ backend thông qua service), ta cần update lại UI dựa vào data mới lấy về.
  2. Sau khi thực thi các hàm timer như setTimeout() hay setInterval()
  3. Hàm Promise.then()
  4. Các hàm bất đồng bộ khác như Websocket.onmessage(), Canvas.blob()…

Angular dùng một thư viện gọi là zone.js để detect change với các hàm bất đồng bộ. Hãy hình dung nếu thiếu zone.js thì chuyện gì sẽ xảy ra? Khi đó lúc khởi tạo component thì Angular vẫn gọi đến change detection và render đúng dữ liệu lên UI với các thao tác đồng bộ, tuy nhiên nếu có hàm bất đồng bộ như get data từ backend, các hàm setTimeout… thì Angular sẽ không biết được là có sự thay đổi giá trị binding.

Vì tầm quan trọng của zone.js mà chúng ta cần hiểu được cơ chế hoạt động của zone.js nếu muốn hiểu sâu hơn về Change Detection.

Thực tế Angular không dùng trực tiếp thư viện zone.js mà wrap thành một thư viện gọi là ngZone. Chúng ta có thể vào source code của một ứng dụng Angular bất kỳ và tận mắt xem nội dung zone.js trong node_modules

node_modules/zone.js/zone.d.ts

Không phải tự dưng mà các dev “to đầu” ở Google lại phải dùng tới zone.js, trong file zone.d.ts ở trên mô tả khá cặn kẽ các tác dụng của zone.js, có thể hiểu nôm na là nó giúp Angular wrap các hàm bất đồng bộ để có thể kiểm soát được trạng thái của chúng đồng thời giữ được ngữ cảnh thực thi (execution context)

A zone by itself does not do anything, instead it relies on some other code to route existing platform API through it. (The zone library ships with code which monkey patches all of the browsers’s asynchronous API and redirects them through the zone for interception.)

Có một ý quan trọng ở đây là thư viện zone này wrap lại các hàm bất đồng bộ trong bộ API của trình duyệt, cái mà bọn nó gọi là “monkey patch”, trong một cái zone để tha hồ quản lý.

Vậy thì quay lại ví dụ ở trên, khi chúng ta có 1 button để tăng giá trị counter, khi click 1 lần thì count tăng 1, nhưng làm sao Angular biết được event on_click của button được kích hoạt và thực thi xong mà render lại UI? Chính vì hàm listener của button được “monkey patch” vào zone mà zone.js sẽ biết được lúc nào event này được trigger, lúc nào thì nó chạy xong hàm callback. Từ đó nó sẽ thêm một hàm detectChanges() vào cuối để kích hoạt change detection!

Nói có sách mách có chứng, hãy đặt một break point vào hàm increaseCount(), ta sẽ thấy rằng zone.js đã handle sự kiện click chuột, chú ý Call Stack ở dưới đây:

Change Detection without zone.js

Để hiểu rõ hơn vai trò của zone.js, hãy thử loại bỏ zone.js ra khỏi ứng dụng xem điều gì sẽ xảy ra. Có hai chỗ cần sửa để disable zone.js

  1. Trong file polyfills.ts comment dòng code import zone.js
  1. Thêm { ngZone: ‘noop’ } vào trong hàm bootstrapModule trong file main.ts

Khi đó việc khởi tạo component vẫn gọi các hàm life cycle như cũ, tuy nhiên khi click vào button “increase count” thì count tăng giá trị nhưng giá trị đó không được update lên view. Nguyên nhân là khi thiếu vắng zone.js, không có ai gọi tới hàm detectChanges để phát hiện sự thay đổi giá trị này. Chúng ta buộc phải trigger bằng tay, dùng hàm ApplicationRef.tick():

Hoặc ChangeDetectorRef.detectChanges(), lưu ý là khi gọi trực tiếp đến hàm này thì các hàm trong Life Cycle của Angular không được gọi đến.

Change Detection Strategy

Nếu bạn tìm hiểu sâu hơn một chút về Change Detection sẽ thấy có 2 “chiến lược” phát hiện thay đổi, đó là:

  1. Default: là CheckAlways, hay là “luôn luôn lắng nghe, luôn luôn thấu hiểu”. Change Detection là tự động dựa vào zone.js như đã nói ở trên.
  2. OnPush: là checkOnce, tức “chỉ một lần này thôi nhé”. Nó chỉ chạy Change Detection lúc đầu tiên khi component khởi tạo. Sau đó chỉ có 2 trường hợp là được trigger Change Detection: một là thay đổi của Input của component (về giá trị nếu là immutable, hoặc reference nếu là mutable, aka, object), hai là từ event của template như onClick, onMousemove…

Tại sao có chế độ OnPush? bởi vì ở chế độ Default Change Detection run rất thường xuyên và có thể ảnh hưởng tới tốc độ trang. Trong nhiều bài viết, OnPush strategy được khuyến cáo dùng để tăng performance, vì Change Detection chỉ được chạy trong 2 trường hợp nói trên. Lưu ý là khi một component được set ở chế độ OnPush, thì tất cả children của nó cũng được set OnPush và không thể override ở component con thành Default được.

Vậy thì điều rút ra ở đây là OnPush strategy sẽ giúp chúng ta kiểm soát được hiệu quả hơn Change Detection, để nó không làm ảnh hưởng tới performance, nhưng ta cần chú ý xử lý việc update changes lên view, chẳng hạn nếu dùng OnPush thì các operation sau không tự động update được giá trị mới lên view

of(1).subscribe((val) => (this.count = val));
setTimeout(() => this.count++, 0)

Nên khi bạn update được data mới rồi nhưng không thấy màn hình thay đổi theo thì đừng hoảng mà hãy handle như chúng ta đã nói ở trên nhé 🙂

Kết bài

Tuy rằng tác giả chưa nói được hết được những điều muốn nói nhưng bài viết cũng đã dài nên xin phép dừng gõ phím ở đây. Cuối cùng có những điều chúng ta cần take away sau bài viết này đó là:

  1. Có tất cả 8 hàm life cycle hooks của component để chúng ta tác động vào component ở những thời điểm thích hợp trong vòng đời của nó.
  2. Các hàm life cycle hooks được khởi chạy khi component được tạo ra, và một có 3 hoặc 4 hàm sẽ được chạy trong quá trình Change Detection.
  3. Change Detection hoạt động dựa vào sự trợ giúp của thư viện zone.js
  4. Có hai chiến lược của Change Detection, trong đó OnPush được khuyên dùng khi ta muốn tối ưu performance trong một số use-case nhất định.

Tham khảo:

2 thoughts on “Lạm bàn về Angular Life Cycle và Change Detection

  1. Bài viết hay và có tâm ạ. Nhưng mà a có thể giải thích về sự liên quan giữa change detection và life cycle ko a.

    Cụ thể là khi change detection được gọi thì nó sẽ xem xét xem loại data nào thay đổi và gọi đến những life cycle method cần thiết đúng không?
    Như trong bài viết, thì nếu loại data mà không phải là input property thì ngOnChanges sẽ không được gọi.

    Tóm lại, ý của e tức là với 1 lần gọi change detection thì có thể kéo theo nhiều life cycle method cũng được gọi.

    Liked by 1 person

  2. Nói ngắn gọn thì change detection là một phần trong life cycle của component/directive, giúp cho chúng ta update những sự thay đổi giá trị property lên view.

    Change detection không xem xét loại data nào thay đổi để gọi hàm life cycle, mà nói đúng hơn là các hàm life cycle sẽ chạy dựa theo điều kiện nhất định. Ví dụ hàm ngOnChanges() được khởi chạy khi change detection phát hiện ra sự thay đổi của Input property, còn hàm ngDoCheck() luôn được chạy sau mỗi change detection cycle.

    Like

Leave a comment