Chào mừng đến với Cafedev! Trong bài viết này, chúng ta sẽ cùng nhau khám phá cơ chế rendering của VueJS. Bạn đã bao giờ tự hỏi VueJS làm thế nào để chuyển đổi một template thành các nút DOM thực sự chưa? Làm thế nào VueJS cập nhật những nút DOM đó một cách hiệu quả? Hãy cùng tìm hiểu chi tiết về cơ chế rendering nội bộ của VueJS trong bài viết này!

Làm thế nào Vue chuyển đổi một template thành các nút DOM thực tế? Làm thế nào Vue cập nhật những nút DOM đó một cách hiệu quả? Chúng ta sẽ cố gắng làm sáng tỏ những câu hỏi này bằng cách khám phá vào cơ chế rendering nội bộ của Vue ở đây.

1. Virtual DOM

Bạn có thể đã nghe về thuật ngữ “virtual DOM”, mà hệ thống rendering của Vue dựa vào.

Virtual DOM (VDOM) là một khái niệm lập trình nơi một biểu diễn “ảo”, hoặc “virtual”, của giao diện người dùng được giữ trong bộ nhớ và đồng bộ với DOM “thực” trong ứng dụng. Khái niệm này được tiên phong bởi React, và đã được áp dụng trong nhiều framework khác với các cài đặt khác nhau, bao gồm cả Vue.

Virtual DOM thực sự là một mẫu thiết kế hơn là một công nghệ cụ thể, vì vậy không có một cài đặt chính thức duy nhất. Chúng ta có thể minh họa ý tưởng bằng một ví dụ đơn giản:

const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* more vnodes */
  ]
}

Ở đây, vnode là một đối tượng JavaScript thuần (một “nút ảo”) đại diện cho một phần tử <div>. Nó chứa tất cả thông tin mà chúng ta cần để tạo ra phần tử thực tế. Nó cũng chứa nhiều nút con hơn, tạo nên nút gốc của một cây DOM ảo.

Một trình biên dịch thời gian chạy có thể duyệt qua cây DOM ảo và xây dựng một cây DOM thực từ đó. Quá trình này được gọi là mount.
Nếu chúng ta có hai bản sao của cây DOM ảo, trình biên dịch cũng có thể duyệt qua và so sánh hai cây, tìm ra sự khác biệt và áp dụng những thay đổi đó vào DOM thực tế. Quá trình này được gọi là patch, còn được biết đến là “diffing” hoặc “reconciliation”.

Lợi ích chính của virtual DOM là nó cho phép nhà phát triển tạo, kiểm tra và sắp xếp cấu trúc UI mong muốn một cách rõ ràng thông qua mã lệnh, trong khi để lại việc manipulation DOM trực tiếp cho trình biên dịch.

2. Render Pipeline

Ở mức cao, điều này xảy ra khi một thành phần Vue được mount:
1. Biên dịch: Các template Vue được biên dịch thành render functions: các hàm trả về cây DOM ảo. Bước này có thể được thực hiện trước thời gian thông qua bước xây dựng, hoặc trực tiếp thông qua trình biên dịch thời gian chạy.
2. Mount: Trình biên dịch thời gian chạy gọi các hàm render, duyệt qua cây DOM ảo được trả về và tạo các nút DOM thực dựa trên nó. Bước này được thực hiện như một hiệu ứng reactive, vì vậy nó theo dõi tất cả các phụ thuộc reactive đã được sử dụng.

3.Patch: Khi một phụ thuộc được sử dụng trong quá trình mount thay đổi, hiệu ứng chạy lại. Lần này, một cây DOM ảo mới, cập nhật được tạo ra. Trình biên dịch thời gian chạy duyệt qua cây mới, so sánh nó với cây cũ và áp dụng các cập nhật cần thiết vào DOM thực tế.

4. Templates vs. Render Functions

Các template của Vue được biên dịch thành các hàm render của virtual DOM. Vue cũng cung cấp các API cho phép chúng ta bỏ qua bước biên dịch template và trực tiếp viết các hàm render. Render functions linh hoạt hơn so với templates khi xử lý logic cực kỳ động, vì bạn có thể làm việc với các vnodes bằng toàn bộ sức mạnh của JavaScript.
Vậy tại sao Vue mặc định khuyến nghị sử dụng templates? Có một số lý do:

  1. Templates gần với HTML thực tế hơn. Điều này làm cho việc tái sử dụng các đoạn mã HTML hiện có, áp dụng các quy ước tốt nhất về tiếp cận, thiết kế với CSS dễ dàng hơn, và dễ hiểu và chỉnh sửa cho các nhà thiết kế.
  2. Templates dễ dàng phân tích tĩnh do cú pháp của chúng xác định rõ hơn. Điều này cho phép trình biên dịch template của Vue áp dụng nhiều tối ưu hóa tại thời gian biên dịch để cải thiện hiệu suất của virtual DOM (chúng tôi sẽ thảo luận về điều này bên dưới).

Trong thực tế, templates đủ cho phần lớn các trường hợp sử dụng trong ứng dụng. Render functions thường chỉ được sử dụng trong các thành phần có thể tái sử dụng cần xử lý logic hiển thị cực kỳ động. Việc sử dụng render functions được thảo luận chi tiết hơn trong Render Functions & JSX.

5. Virtual DOM Dựa Trên Trình Biên Dịch

Thực hiện virtual DOM trong React và hầu hết các triển khai virtual-DOM khác là hoàn toàn thời gian chạy: thuật toán phối hợp không thể đưa ra bất kỳ giả định nào về cây virtual DOM đầu vào, vì vậy nó phải duyệt qua toàn bộ cây và diff các props của mỗi vnode để đảm bảo tính chính xác. Ngoài ra, ngay cả khi một phần của cây không thay đổi, các vnode mới vẫn luôn được tạo ra cho chúng trên mỗi lần render lại, dẫn đến áp lực về bộ nhớ không cần thiết. Điều này là một trong những khía cạnh nhận xét nhiều nhất của virtual DOM: quá trình phối hợp hơi brute-force hi sinh hiệu suất để đổi lấy tính rõ ràng và đúng đắn.

Nhưng không nhất thiết phải như vậy. Trong Vue, framework kiểm soát cả trình biên dịch và thời gian chạy. Điều này cho phép chúng ta thực hiện nhiều tối ưu hóa tại thời gian biên dịch mà chỉ một renderer chặt chẽ có thể tận dụng được. Trình biên dịch có thể phân tích tĩnh template và để lại gợi ý trong mã được tạo ra để thời gian chạy có thể tận dụng các shortcut khi có thể. Đồng thời, chúng tôi vẫn bảo toàn khả năng cho người dùng giảm xuống lớp hàm render để có sự kiểm soát trực tiếp hơn trong các trường hợp đặc biệt. Chúng tôi gọi phương pháp tiếp cận kết hợp này là Compiler-Informed Virtual DOM.

Dưới đây, chúng tôi sẽ thảo luận về một số tối ưu hóa chính được thực hiện bởi trình biên dịch template Vue để cải thiện hiệu suất thời gian chạy của virtual DOM.

5.1 Static Hoisting

Rất nhiều khi sẽ có các phần trong một template không chứa bất kỳ ràng buộc động nào:

<div>
  <div>foo</div> <!-- hoisted -->
  <div>bar</div> <!-- hoisted -->
  <div>{{ dynamic }}</div>
</div>

Inspect in Template Explorer
Các div foobar là tĩnh – việc tái tạo các vnode và diff chúng trên mỗi lần render lại là không cần thiết. Trình biên dịch Vue tự động kéo các cuộc gọi tạo vnode của chúng ra khỏi hàm render, và tái sử dụng các vnode cùng một lần render. Renderer cũng có thể bỏ qua hoàn toàn việc diff chúng khi nhận thấy vnode cũ và vnode mới là cùng một vnode.

Ngoài ra, khi có đủ các phần tĩnh liên tiếp, chúng sẽ được nén thành một vnode tĩnh duy nhất chứa chuỗi HTML đơn giản cho tất cả các node này (Example). Các vnode tĩnh này được mount bằng cách thiết lập innerHTML trực tiếp. Chúng cũng lưu cache các node DOM tương ứng của chúng trên lần mount ban đầu – nếu cùng một phần nội dung được tái sử dụng ở nơi khác trong ứng dụng, các node DOM mới được tạo bằng cloneNode() native, rất hiệu quả.

5.2 Patch Flags

Đối với một phần tử đơn có các ràng buộc động, chúng ta cũng có thể suy luận được rất nhiều thông tin từ nó tại thời gian biên dịch:

<!-- class binding only -->
<div :class="{ active }"></div>

<!-- id and value bindings only -->
<input :id="id" :value="value">

<!-- text children only -->
<div>{{ dynamic }}</div>

Inspect in Template Explorer
Khi tạo mã hàm render cho các phần tử này, Vue mã hóa loại cập nhật mỗi trong số chúng trực tiếp trong cuộc gọi tạo vnode:


createElementVNode("div", {
  class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)

Đối số cuối cùng, 2, là một cờ patch. Một phần tử có thể có nhiều cờ patch, sau đó sẽ được gộp thành một số duy nhất. Runtime renderer sau đó có thể kiểm tra chống lại các cờ bằng cách sử dụng các phép toán bitwise để xác định liệu nó cần làm công việc nhất định hay không:

if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
  // update the element's class
}

Kiểm tra bitwise cực kỳ nhanh chóng. Với các cờ patch, Vue có thể làm ít công việc nhất cần thiết khi cập nhật các phần tử có các ràng buộc động.
Vue cũng mã hóa loại các thành phần con mà một vnode có. Ví dụ, một template có nhiều nút gốc được đại diện dưới dạng một fragment. Trong hầu hết các trường hợp, chúng ta biết chắc chắn rằng thứ tự của các nút gốc này sẽ không bao giờ thay đổi, vì vậy thông tin này cũng có thể được cung cấp cho runtime dưới dạng một cờ patch:


export function render() {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    /* children */
  ], 64 /* STABLE_FRAGMENT */))
}

Do đó, runtime có thể hoàn toàn bỏ qua việc so sánh thứ tự của các con trong fragment gốc.

5.3 Tree Flattening

Nhìn lại vào mã được tạo ra từ ví dụ trước, bạn sẽ thấy rằng gốc của cây virtual DOM được trả về được tạo bằng cách sử dụng một cuộc gọi createElementBlock() đặc biệt:


export function render() {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    /* children */
  ], 64 /* STABLE_FRAGMENT */))
}

Theo quan điểm, một “block” là một phần của template có cấu trúc bên trong ổn định. Trong trường hợp này, toàn bộ template có một block duy nhất vì nó không chứa bất kỳ chỉ thị cấu trúc nào như v-ifv-for.
Mỗi block theo dõi bất kỳ node con (không chỉ là con trực tiếp) nào có cờ patch. Ví dụ:


<div> <!-- root block -->
  <div>...</div>         <!-- not tracked -->
  <div :id="id"></div>   <!-- tracked -->
  <div>                  <!-- not tracked -->
    <div>{{ bar }}</div> <!-- tracked -->
  </div>
</div>

Kết quả là một mảng được làm phẳng chỉ chứa các node con động:

div (block root)
- div with :id binding
- div with {{ bar }} binding


Khi thành phần này cần phải render lại, nó chỉ cần duyệt cây được làm phẳng thay vì toàn bộ cây. Điều này được gọi là Làm phẳng cây, và nó giảm đáng kể số lượng node cần phải duyệt trong quá trình hòa giải virtual DOM. Bất kỳ phần tĩnh nào của template đều được bỏ qua một cách hiệu quả.
v-ifv-for tạo ra các block node mới:

<div> <!-- root block -->
  <div>
    <div v-if> <!-- if block -->
      ...
    <div>
  </div>
</div>

Một block con được theo dõi bên trong mảng của các node con động của block cha. Điều này giữ nguyên cấu trúc ổn định cho block cha.

5.6 Tác động đến Hydration SSR

Cả cờ patch và làm phẳng cây cũng cải thiện đáng kể hiệu suất Hydration SSR của Vue:
– Hòa giải một phần tử có thể diễn ra theo các đường dẫn nhanh dựa trên cờ patch của vnode tương ứng.
– Chỉ các block node và các node con động của chúng cần được duyệt trong quá trình hòa giải, hiệu quả đạt được hòa giải một phần ở cấp độ template.

Trên đây là những điều cơ bản về cơ chế rendering của VueJS mà chúng ta đã tìm hiểu qua bài viết này trên Cafedev. Từ việc sử dụng virtual DOM đến các tối ưu hóa thông qua compiler, VueJS đã cho chúng ta cái nhìn sâu sắc về cách nó hoạt động bên trong. Hy vọng rằng những kiến thức này sẽ giúp bạn hiểu rõ hơn về cách VueJS xử lý rendering và áp dụng vào công việc của mình. Hãy tiếp tục khám phá và áp dụng những điều bạn học được từ bài viết này trong dự án của mình trên Cafedev!

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!