How does the Vue Template work?

Vue.js

Introduction

Vue.js (Vue) - là một JavaScript framework được sử dụng để xây dựng giao diện người dùng và đang nhận được rất nhiều sự quan tâm của cộng đồng lập trình trong thời gian gần đây. Vue khá dễ tiếp cận cho những người mới tìm hiểu về nó (bản thân mình cũng mới làm việc với Vue khoảng hơn 4 tháng trở lại đây !?). Từ năm 2015, Vue nhận được sự support rất lớn từ cộng đồng Laravel và trở thành Front-end preset mặc định khi cài đặt Laravel; Laravel Mix - A Wrapper Around Webpack cũng hỗ trợ Vue mặc định.

Trong bài viết ngắn này mình sẽ trình bày về một số vấn đề liên quan đến Template trong Vue cũng như cách mà Vue xử lý Template. Bài viết chắc chắn sẽ có nhiều sai sót do mình chỉ hoàn thành nó trong khoảng thời gian hơn 1 ngày 😄

Template In a Nutshell

Trong phần đầu của bài viết, chúng ta sẽ tìm hiểu một cách khá tổng quát về quá trình chuyển đổi Template cũng như xử lý những thay đổi (từ những tương tác bên phía người dùng) trong Vue. Chúng ta có thể chia quá trình trên thành hai giai đoạn chính: Initial RenderSubsequent Updates với các bước chính trong mỗi giai đoạn như sau:

  • Initial Render
    • Compile Template đã được định nghĩa thành Render Function.
    • Thực thi Render Function để thu về Virtual DOM.
    • Sử dụng Virtual DOM để tạo ra Actual DOM.

Vue Initial Render

  • Subsequent Updates
    • Khi có thay đổi về data, tạo ra một Virtual DOM hoàn toàn mới.
    • Diff Virtual DOM cũ và mới ta thu được DOM updates.
    • Áp dụng những thay đổi về DOM lên Actual DOM.

Vue Subsequent Updates

Trong lần render đầu tiên, Vue sẽ sử dụng template đã được định nghĩa và compile template đó thành Render Function sử dụng Vue Compiler và thực thi Render Function đó để thu về Virtual DOM (chúng ta sẽ bàn về Virtual DOM trong phần sau của bài viết). Virtual DOM sẽ được sử dụng để tạo ra Actual DOM (native DOM). Khi dữ liệu bên trong component thay đổi Render Function sẽ được gọi lại và một Virtual DOM mới sẽ được tạo ra. Vue sẽ sử dụng một diffing algorithm để tìm ra sự khác biệt giữa hai instance của Virtual DOM (DOM updates) và áp dụng những thay đổi đó lên Actual DOM mà không phải tái tạo Actual DOM từ đầu.

Nếu bạn đã từng sử dụng React, trong mỗi component chúng ta sẽ định nghĩa nội dụng chi tiết cho Render Function (thường sử dụng cú pháp JSX để đơn giản hóa). Tuy nhiên trong Vue chúng ta rất ít khi định nghĩa Render Function một cách trực tiếp, thay vào đó Vue đưa ra khái niệm Template. Có ba cách chính để định nghĩa template cho một component trong Vue:

  • In-browser Template: template sẽ được compile khi component được load trên browser.
  • Single File Component: sử dụng file có định dạng .vue và định nghĩa template bên trong thẻ <template></template>, cách này mang lại khá nhiều lợi ích tuy nhiên nó yêu cầu phải thực hiện một build step sử dụng Webpack chẳng hạn để chuyển đổi .vue file thành JavaScript, do trình duyệt trong hiểu được định dạng .vue.
  • Pure Render Function: sử dụng JavaScript thuần để định nghĩa template. Performance của phương pháp này khá cao tuy nhiên việc viết template sẽ trở nên khó khăn hơn so với hai cách đề cập bên trên.

In-browser Template

Có lẽ khi mới bắt đầu làm quen với Vue, chúng ta thường sử dụng cách này để định nghĩa template. Phương pháp này yêu cầu chúng ta sử dụng Standalone Version của Vue, khi đó template của component sẽ được compile tại browser. Template sẽ được định nghĩa dưới dạng một string sử dụng template option trong Vue component. Dưới đây là một số ví dụ minh họa:

Điểm khác biệt cơ bản giữa StandaloneRuntime-only version của Vue đó là version đầu sẽ chứa Vue compiler do đó template không cần phải được pre-compiled trước mà sẽ được compile on the fly khi component được gọi đến.

Cách thông thường và đơn giản nhất là định nghĩa một string bên trong template option của Vue component. Ở ví dụ bên dưới chúng ta có một component với tên là greeting, nhận vào một prop là person và template đơn giản in ra một câu chào tới người đó.

Vue.component('greeting', {
  template: '<div> Hello, {{ person }}!</div>',
  props: ['person']
})

Sử dụng ES2015 template literals nếu muốn nội dung template dễ đọc hơn:

Vue.component('greeting', {
  template: `
    <div>
      <strong>Hello, {{ person }}!</strong>
    </div>
  `,
  props: ['person']
})

Tách biệt nội dung template ra một file riêng biệt và require file dó bên trong Vue component, phương pháp này yêu cầu chúng ta sử dụng module bundler như Webpack hay Browserify:

// template.html.js
module.exports = `
  <div> Hello, {{ person }}!</div>
`

Vue.component('greeting', {
  template: require('./template.html'),
  props: ['person']
})

Sử dụng x-template, cách này thường được sử dụng trong phiên bản 1.* của Vue. Phương pháp này chỉ phù hợp với những dự án nhỏ vì việc quản lý template sẽ trở nên rất khó khăn khi số lượng component tăng lên. Trên thực tế phương pháp này chỉ nên sử dụng khi chúng ta muốn thực hiện một số thử nghiệm với các tính năng của Vue, không nên sử dụng trong dự án thực tế.

x-template còn được gọi là In-DOM template do template sẽ được viết trực tiếp trong HTML. Template sẽ được parse bởi browser trước, Vue sau đó sẽ lấy ra nội dung của template thông qua outerHTML và truyền xuống cho Vue Compiler !? Tuy nhiên cần chú ý rằng HTML là case-sensitive, do đó nội dung của template sẽ được normalize trước khi được truyền cho Vue. Do vậy khi sử dụng In-DOM template chúng ta sẽ không sử dụng được camel-case property cho component. Thông thường trong Vue chúng ta sẽ sử dụng dash-separated string (kebab-case) cho tên của prop, và access prop dưới dạng camel-case trong JavaScript code.

// index.html
<script type="text/x-template" id="template">
  <div>Hello, {{ person }}!</div>
</script>
Vue.component('greeting', {
  template: '#template',
  props: ['person']
})

Sử dụng inline-template. Nếu ứng dụng của bạn không phải là Single Page Application (SPA), sẽ có trường hợp bạn sẽ phải mix Vue component bên trong template của một framework khác như Blade của Laravel chẳng hạn. Đôi khi chúng ta cần nội dung template của component trở nên dynamic, sử dụng inline-template là một cách để thực hiện việc này. Hiểu một cách đơn giản Vue component sẽ là nơi chúng ta xử lý các core logic, còn việc thể hiện logic đó sẽ được thực hiện khi component đó được sử dụng. Phương pháp này không được khuyến khích sử dụng do nó gây ra khó khăn khi muốn hiểu về scope của component (Dữ liệu này từ đâu ra? Nó được tính toán như thế nào?)

Vue.component('greeting', {
  props: ['person']
})
// index.blade.php
<greeting person="{{ $name }}" inline-template>
  <div>Hello, {{ person }}!</div>
</greeting>
  • Pros: phương pháp định nghĩa template này khá đơn giản và không yêu cầu chúng ta phải có kiến thức về một số công cụ module bundler như Webpack hay Browserify.
  • Cons: đối với các dự án lớn, performance sẽ bị hạn chế khá nhiều do việc chuyển đổi template diễn ra ngay trên trình duyệt. Initial Render sẽ diễn ra chậm hơn do toàn bộ template cần phải được compile sang JavaScript trước.

Single File Component

Single File Component đã xuất hiện từ phiên bản 1.* của Vue, tuy nhiên trong phiên bản 2.* nó đã được thay đổi và cải thiện khá nhiều. Phương pháp sử dụng string template nói trên sẽ chỉ thích hợp cho các dự án vừa và nhỏ. Nếu dự án ở mức lớn hơn, hoặc phần frontend của dự án được thực hiện hầu hết trên JavaScript, phương pháp đó sẽ mang lại khá nhiều điều bất lợi:

  • Các component sẽ được đăng ký globally sử dụng Vue.component(), do đó tên của các component phải là duy nhất.
  • String template sẽ trở nên khó quản lý hơn khi nội dung template trở lên phức tạp (và thường sẽ không được highlight bởi các text editor hoăc IDE).
  • CSS sẽ nằm tách biệt ra khỏi component.
  • Giới hạn các kĩ thuật được sử dụng do component không trải qua một build step nào cả (chỉ sử dụng HTML và ES5 mà thôi - nếu muốn tương thích trên hầu hết các trình duyệt)

Để khắc phục những bất lợi trên, chúng ta sẽ thêm một bước trung gian nhằm chuyển đổi component sang JavaScript mà trình duyệt có thể hiểu được. Nội dung của một component sẽ được lưu trong một file với định dạng là .vue với ba phần chính là: template, scriptstyle. Chúng ta sẽ sử dụng một số công cụ như Webpack với vue-loader để chuyển đổi các component trên sang dạng JavaScript truyền thống. Dưới đây là một ví dụ minh họa:

// Greeting.vue
<template>
  <div>
    Hello <span v-text="name" class="name"></span>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        name: 'Steve Jobs'
      }
   }
 }
</script>

<style lang="sass" scoped>
  .name
    font-weight: bold
    color: #FF0000
</style>

Như chúng ta đã thấy trong ví dụ phía trên, chúng ta không bị giới hạn phải sử dụng ES5 mà có thể sử dụng ES6 cùng với đó là việc sử dụng SASS để định nghĩa style. Nếu nhìn vào component trong ví dụ phía trên, bạn có thể đặt ra câu hỏi là liệu chúng ta có đang vi phạm quy tắc SoC (Separation of Concerns) khi mà chúng định nghĩa cả HTML, JavaScript và CSS trong cùng một file. Theo những cách truyền thống thì ba loại file đó sẽ được tách biệt nhau.

Phương pháp lưu trữ tách biệt HTML, JavaScript và CSS sẽ phù hợp hơn với các ứng dụng truyền thống. Hiện tại, hầu hết các công việc sẽ được xử lý bên phía client, do đó việc gộp các logic liên quan đến một component bên trong một file sẽ làm cho mọi thứ trở nên rõ ràng và dễ quản lý hơn. Thay vì chia codebase theo loại file, chúng ta sẽ chia theo component và thực hiện việc quản lý sự tương tác giữa các component với nhau.

  • Pros: tận dụng được các lợi ích khi chia các component thành các modules khác nhau. Việc sử dụng các công cụ bundler sẽ giúp cho việc tối ưu code tốt hơn (chúng ta có thể sử dụng code-splitting hoặc tree shaking trong Webpack chẳng hạn). Một ưu điểm nữa là chúng ta có thể sử dụng những công nghệ và kĩ thuật mới mà không cần lo lắng về đầu ra.
  • Cons: phương pháp này yêu cầu chúng ta phải có những kiến thức về module trong JavaScript cũng như các sử dụng các module bundler phù hợp để chuyển đổi .vue file sang định dạng mà trình duyệt có thể tiếp nhận.

Pure Render Function

Nếu chúng ta muốn ứng dụng đạt được performance cao và không cần phải sử dụng đến các công cụ module bundler, tương tự như React chúng ta có thể định nghĩa render function trong vue và loại bỏ template. Trên thực tế, hai phương pháp quản lý template nói trên chỉ là sự trừu tượng hóa bên trên Render Function giúp cho việc phát triển trở nên dễ dàng hơn. Nếu chúng ta không định nghĩa Render Function của một component một cách trực tiếp, Vue sẽ thực hiện việc compile template sang Render Function. Dưới đây là một ví dụ đơn giản:

Vue.component('greeting', {
  props: ['person'],
  render(h) {
    return h('div', `Hello, ${this.person}`)
  }
});

Trong ví dụ trên thay vì định nghĩa template dưới dạng string hoặc sử dụng .vue - Single File Component, chúng ta sẽ định nghĩa một render function cho component với tham số đầu vào là h. h thực chất là một cách viết ngắn gọn cho vm.$createElement. Ở đây chúng ta sẽ tạo ra một div tag với nội dung là một câu chào đơn giản. Chúng ta sẽ tìm hiểu rõ hơn về Render Function trong phần sau của bài viết.

  • Pros: tối ưu được performance.
  • Cons: rất khó để hiểu được nội dung của Render Function do nó không được viết dưới dạng HTML-like. Trên thực tế chúng ta cũng có thể sử dụng JSX để định nghĩa nội dung của Render Function tương tự như React.

Virtual DOM vs. Actual DOM

Virtual DOM không còn là một khái niệm mới vì nó đã được đề cập đến khá nhiều khi React sử dụng nó để tính toán các thay đổi cần thiết trên Actual DOM. Hãy bắt đầu bằng một ví dụ đơn giản, giả sử chúng ta muốn tạo một thẻ div sử dụng Actual DOM và Virtual DOM:

// Actual DOM
var divElement = document.createElement('div');
console.log(divElement.toString()); // -> [object HTMLDivElement]

// Virtual DOM (trong Vue)
var divElement = vm.$createElement('div');
console.log(divElement.toString()); // -> { tag: 'div', data: { attrs: {}, ...}, children: [] }

Đối với trường hợp sử dụng Actual DOM, chúng ta đã sử dụng DOM Native API để tạo ra một real div element có thể được sử dụng để append vào document. Tuy nhiên khi chúng ta gọi hàm toString() trên div element vừa tạo, kết quả trả về sẽ là một string - [object HTMLDivElement], điều này thường xảy ra khi chúng ta gọi toString() trên các Browser Native Object. Điều đó chứng tỏ rằng chúng ta đã đi ra khỏi giới hạn JavaScript và access sâu hơn đến các native components mà trình duyệt cung cấp (C++ maybe!?). Đối với JavaScript, việc tạo, update hoặc truy cập các native objects là khá expensive.

Tuy nhiên đối với trường hợp sử dụng Virtual DOM (trong Vue), kết quả trả về sẽ là một plain JavaScript object với các thông số như tên của element, attributes của nó là gì và một mảng các Virtual Node con của nó nếu có. Do vậy Virtual DOM sẽ chỉ hoạt động trong môi trường JavaScript mà không cần access đến các native components của trình duyệt. Khác với Actual DOM, thao tác trên JavaScript object sẽ nhanh và hiệu quả hơn so với các native objects. Do đó, chúng ta có thể gọi Render Function nhiều lần mà không làm ảnh hưởng đến performance. vm.$createElement() là một API mức thấp của Vue, thường chúng ta sẽ sử dụng nó khi chúng ta định nghĩa Render Function một cách trực tiếp.

Trước khi tìm hiểu rõ hơn về Virtual DOM, chúng ta sẽ đề cập đến một số hạn chế của việc sử dụng Actual DOM (HTML DOM):

  • Re-rendering toàn bộ hoặc một phần lớn Actual DOM là khá tốn kém tài nguyên và phức tạp.
  • Việc giữ cho Actual DOM đồng bộ với dữ liệu (state) là khá phức tạp và dễ xảy ra lỗi.

Việc tạo, và thay đổi Plain JavaScript Object (Virtual DOM) sẽ nhanh và hiệu quả hơn rất nhiều do các thao tác đó chỉ xảy ra trong khuôn khổ JavaScript thay vì trong C++ implementation của trình duyệt.

Vậy Virtual DOM là gì và tại sao chúng ta nên sử dụng Virtual DOM thay vì thao tác trực tiếp trên Actual DOM?

Một cách tổng quát, Virtual DOM là một định dạng dữ liệu JavaScript nhẹ được dùng để thể hiện nội dung của Actual DOM tại một thời điểm nhất định nào đó.

Virtual DOM Tree

Trong định nghĩa về Virtual DOM nói trên, có hai điều chúng ta cần lưu ý:

  • Virtual DOM là một định dạng dữ liệu JavaScript nhẹ (so-called Lightweight JavaScript Data Format) có nghĩa là việc tạo mới Virtual DOM là rất cheap , không có nhiều overheads và không tốn nhiều tài nguyên hệ thống.
  • Virtual DOM một khi đã được tạo ra thì sẽ không được thay đổi sau đó. Để dễ hình dung, mỗi instance của Virtual DOM sẽ như một screenshoot của Actual DOM tại một thời điểm cố định nào đó.

Virtual DOM is immutable and should be cheap to create.

Như vậy chúng ta đã nắm được định nghĩa của Virtual DOM, một câu hỏi đặt ra là Virtual DOM có phải là nguyên làm cho thư viện như React hoặc framework như Vue có performance rất cao không? Câu trả lời là KHÔNG. Việc tạo và sử dụng Virtual DOM cũng có những overhead nhất định. Tuy nhiên Virual DOM mang lại một số lợi ích mà chúng ta không thấy khi sử dụng Actual DOM:

  • Việc tách biệt logic liên quan đến việc rendering ra khỏi Actual DOM cho phép Vue hay React chạy được trên nhiều môi trường khác nhau thay vì chỉ một môi trường duy nhất là trình duyệt (React Navtive hoặc SSR - Server-Side Rendering cho cả React và Vue). Chúng ta biết rằng Virtual DOM trên thực tế chỉ là một JavaScript object do đó chúng ta chỉ cần một môi trường cho chép chạy JavaScript và việc sử dụng Virtual DOM là hoàn toàn khả thi. Một số ví dụ có thể nói đến ở đây là: SSR sử dụng NodeJS hoặc embedded JavaScript khi xây dựng các ứng dụng native cho nền tảng Web cũng như Mobile. Công việc của chúng ta là thay đổi generation algorithm cho từng môi trường cụ thể nhằm thu được các output mong muốn cho từng môi trường đó.
  • Sử dụng Virtual DOM cho phép chúng ta tính toán các thay đổi trên đó (just JavaScript) và áp dụng đồng thời các thay đổi đó lên Actual DOM khi cần thiết. Phương pháp này sẽ hiệu quả hơn khá nhiều so với việc access Actual DOM element (sử dụng jQuery chẳng hạn) và tính toán các thay đổi trên đó.

Nếu hiểu một cách đơn giản nhất, Server Side Rendering thực chất là việc stringify Virtual DOM để thu được HTML 😄

Server-Side Rendering còn khá mới với Vue và đang trong quá trình cải tiến. Nếu quan tâm và muốn tìm hiểu về SSR trong Vue, bạn có thể tham khảo Nuxt.js. Viblo cũng đang trong quá trình chuyển đổi sang SSR thay vì sử dụng mô hình truyền thống.

Việc đi sâu vào cách thức hoạt động cũng như cách mà Virtual DOM được implement là vượt qua khuôn khổ của bài viết này. Bản thân mình cũng không hiểu hết được logic bên trong Virtual DOM (việc này cũng không cần thiết). Source code của Vue Virtual DOM bạn có thể tham khảo trên github repository của Vue.

The Render Function

Từ phiên bản 2.0, Vue cho phép chúng ta định nghĩa Render Function một cách trực tiếp. Trong hầu hết các trường hợp, tạo template bằng cách sử dụng String-based Template hoặc Single File Component sẽ đơn giản và hiệu quả hơn. Tuy nhiên trong một vài trường hợp chúng ta cần sử dụng JavaScript thuần túy để định nghĩa template cho một component đặc biệt là các Functional Components.

Vue Render Function

Trong official guide của Vue có đưa ra một ví dụ về việc tạo một component với nhiệm vụ là render HTML headings. Tài liệu có đưa ra hai cách sử dụng template thông thường và sử dụng Render Function. Chúng ta có thể thấy sử dụng Render Function trong trường hợp này đơn giản hơn rất nhiều. Dưới đây là đoạn code được lấy ra từ ví dụ đó:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // tag name
      this.$slots.default // array of children
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

Như chúng ta có thể thấy, anchored-heading component không hề định nghĩa template qua template option, chúng ta cũng không thấy bất kỳ đoạn mã HTML nào cả. Thay vào đó chúng ta có một render function mới đối số đầu vào createElement. Nếu quan sát nội dụng của Render Function, chúng ta có thể thấy nó có nhiệm vụ tạo một heading element (h1 -> h6) dựa vào tham số đầu vào là level, với dữ liệu là danh sách các element nằm trong thẻ heading đó (truy cập thông qua this.$slots.default). Để làm việc với Render Function, chúng ta cần nắm chắc API mà Vue cung cấp.

Render Function là một function mà khi thực thi sẽ trả về một instance của Virtual DOM (Render Function returns Virtual DOM).

Functional Components là những component không chứa và quản lý state bên trong chính nó hoặc được truyền thông qua props cũng như các lifecycle methods. Functional Component trên thực tế là các function nhận vào một số dữ liệu qua props và render dữ liệu đó. Functional Components

From Template to Render Function

Như đã đề cập ở phần đầu của bài viết, template trên thực tế là một lớp abstraction bên trên Render Function. Vậy làm cách nào chúng ta có thể chuyển đổi template thành các đoạn mã JavaScript mà trình duyệt có thể hiểu được. Vue Compiler là câu trả lời cho câu hỏi trên.

[Vue Template] --> [Vue Compiler] --> [Render Function]

Template to Render Function

Mình còn nhớ khi học môn Compiler Construction ở trường đại học và được thực hành viết compiler cho một ngôn ngữ khá dị là KPL (Kid's Programming Language!?). Mình cũng không nhớ rõ định nghĩa về compiler, nhưng nói chung compiler là một chương trình máy tính giúp chuyển đổi source code được viết bởi các ngôn ngữ lập trình cấp cao (high level language) thành dạng binary mà máy tính có thể hiểu được. Compiler chứa khá nhiều công đoạn, cụ thể là:

  • Lexcial Analyzer: source code sẽ được đọc từ trái sang phải và sẽ được parse thành những tokens (token stream). Mỗi token sẽ chứa một ý nghĩa nào đó (keywords, operators, expressions,...)
  • Parser: nhóm các tokens theo một cấu trúc ngữ nhĩa cụ thể nào đó (BNF chẳng hạn) sử dụng một số phương pháp như Predictive Parsing, To-down backtrack parsing, hay Recursive Descent Parsing !?!?
  • Sematic Analysis: kiểm tra source code để tìm ra các lỗi cú pháp hoặc lỗi ngữ nghĩa.
  • Intermediate Code Generation: chuyển đổi source code sang một dạng trung gian (không phụ thuộc vào môi trường) - dạng tree chẳng hạn.
  • Code Optimization: tối ưu intermediate code.
  • Code Generation: chuyển đổi intermediate code sang machine-dependent code (assembly code hay virtual machine code)

Quay trở lại với vấn đề chính, Vue Compiler cũng sẽ trải qua rất nhiều bước trước khi trả về Render Function từ Template, cụ thể là:

[Vue Template] --> [Parser] --> [AST (Abstract Syntax Tree)] --> [Optimizer] --> [Codegen] --> [Render Function]

Như chúng ta có thể thấy trong sơ đồ trên, trong Vue compiler chúng ta sẽ bỏ qua bước đầu tiên theo lý thuyết về complier - Lexical Analyzer (hay Tokenizer); thay vào đó Vue Template sẽ được parse thành một cấu trúc dữ liệu - AST biểu diễn cấu trúc của template. Do vue template thường ở dạng HTML (ở mức syntax), chúng ta có thể sử dụng một số thư viện HTML Parser có sẵn để chuyển đổi template sang AST.

// Template
<div>{{ message }}</div>

// AST
{
  type: ELEMENT,
  tag: 'div',
  attrs: {},
  children: [
    type: EXPRESSION,
    value: 'message'
  ]
}

// Render Function
"return createElement('div', {}, [createText(message)])"

AST sẽ có cấu trúc tương tự với Virtual DOM, tuy nhiên nó sẽ chứa raw information về template. AST sau đó sẽ được chuyển đổi thành JavaScript với sự trợ giúp của Codegen (hay Code generator). Phía trên là một ví dụ đơn giản minh họa cho quá trình chuyển đổi từ template sang Render Function.

Template to Render Function Examples

Trong phần này chúng ta sẽ cùng làm một số thí nghiệm nho nhỏ với Vue Template sử dụng Vue Template Explorer. Trước tiên chúng ta cần tìm hiểu qua một số định nghĩa sau:

  • _c: là một alias của $createElement instance method, function này trả về một VNode instance.
  • _v: tạo một text node - $createText
  • _s: normalize string
  • _l: helper function sử dụng để render một danh sách
  • e(): empty VNode -- rendering thành comment text trong Actual DOM thường xảy ra khi chúng ta sử dụng v-if directive.

Trước khi đọc tiếp phần bên dưới, bạn nên dành một chút thời giản để tìm hiểu về createElement function của Vue read here

Ok, chúng ta sẽ bắt đầu bằng một template đơn giản - Output a string
// Template
<div id="greeting">{{ message }}</div>

// Render Function using with()
function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "greeting"
      }
    }, [_v(_s(message))])
  }
}

// Render Function not using with()
function render() {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return _c('div', {
    attrs: {
      "id": "greeting"
    }
  }, [_vm._v(_vm._s(_vm.message))])
}

Trong ví dụ đầu tiên template của chúng ta đơn giản là một string interpolation của message data property. message sẽ được chứa bên trong một div tag với ID là greeting. Sau khi copy-paste đoạn template trên vào Vue Template Explorer, chúng ta có hai phiên bản của Render Function với sự khác biệt ở việc sử dụng hàm with() hay không. Nội dụng của Render Function này khá đơn giản, tạo một div element với ID attribute là greeting chứa một node con là một text node với nội dung là message. Lưu ý trong template chúng ta sử dụng syntax {{ }} nên message sẽ được escaped trước khi được truyền cho $createText function.

Vậy sự khác biệt trong việc sử dụng hay không sử dụng with() ở đây là gì? Câu trả lời ở đây sử dụng with() sẽ đơn giản hóa quá trình compiling, khi không sử dụng with() mọi lời gọi đến các API hay data property sẽ đều phải thông qua _vm (Vue instance). Trên thực tế khi sử dụng một số công cụ module bundler như Webpack (cùng với Vue loader) Vue sẽ thực hiện quá trình strippling with để loại bỏ đi with() function, do đó compiled code sẽ ở dạng strict-mode.

Vue proxy tất cả các data propery bên trong data() function của component, do đó chúng ta có thể access chúng như một property của _vm (như _vm.message trong ví dụ trên).

Một lưu ý nữa là khi access data property như trên, getter của property đó sẽ được gọi đến. Vue sẽ đăng ký một watcher cho property trong quá trình đó từ đó Vue sẽ biết Render Function phụ thuộc vào những data property nào !? Trong các ví dụ tiếp theo chúng ta sẽ chỉ sử dụng stripped with version của Render Function cho việc minh họa.

Conditionals

// Template
<div v-if="ok">{{ message }}</div>

// Render Function
function render() {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return (_vm.ok) ? _c('div', [_vm._v(_vm._s(_vm.message))]) : _vm._e()
}

v-if là một directive được sử dụng khá nhiều trong Vue. Khác với v-show, v-if sẽ tạo hoặc loại bỏ element ra khỏi DOM thay vì toggle display attribute của element đó. Trong ví dụ này nếu ok trả về một truthy value thì message sẽ được hiển thị và ngược lại.

Trong JavaScript một biến là truthy khi giá trị của nó KHÔNG PHẢI0, '', null, undefined, false

Nếu ok là một truthy value, chúng ta sẽ render div element tương tự như trong ví dụ đầu tiên, ngược lại chúng ta sẽ render một empty virtual node (VNode). Lý do chúng ta trả về một empty VNode là giúp cho việc diffing hay patching Virtual DOM trở nên dễ dàng hơn. Giả sử chúng ta có một danh sách với hai phiên bản của nó trong hai instance của Virtual DOM. Nếu chúng ta muốn tìm ra sự khác biệt giữa hai phiên bản đó, sẽ dễ dàng hơn nếu hai danh sách đó có cùng độ dài.

Empty Virtual Node sẽ được render thành comment trên Actual DOM

List Rendering

// Template
<ul>
  <li v-for="product in products" v-text="product.name"></li>
</ul>

// Render Function
function render() {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return _c('ul', _vm._l((_vm.products), function(product) {
    return _c('li', {
      domProps: {
        "textContent": _vm._s(product.name)
      }
    })
  }))
}

Trong ví dụ này chúng ta cũng sẽ sử dụng một directive khá quen thuộc - v-for để hiển thị một danh sách (unordered list) các sản phẩm. Sự khác biệt ở đây nằm ở việc sử dụng _l() function, có thể hiểu function này tương tự như Array.prototype.map(). Mỗi sản phẩm trong danh sách các sản phẩm đã cho sẽ được map đến một instance của VNode (virtual li element). Chú ý trong ví dụ này thay vì sử dụng string interpolation - {{ product.name }} chúng ta sử dụng một directive v-text với chức năng tương tự. Do đó thay vì tạo ra một text node, Vue sẽ truyền một DOM property cho virtual li element được thể hiện trong domProps option của data object của createElement() function.

Event Handler

// Tempate 
<button v-on:click="counter += 1">Add 1</button>

// Render Function
function render() {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return _c('button', {
    on: {
      "click": function($event) {
        _vm.counter += 1
      }
    }
  }, [_vm._v("Add 1")])
}

Counter - một ví dụ minh họa khá quen thuộc khi nhắc đến event handling trong Vue. Điều chúng ta cần chú ý là tất cả các event handler sẽ được nhóm dưới key on trong data object của createElement. Với key là tên của event và value là handler tương ứng.

Conclusion

Trong bài viết này mình có trình bày một cách khái quát về Template trong Vue, cách sử dụng cũng như cách hoạt động của Template ở mức cơ bản nhất. Mình cũng đã trình bày qua về Virtual DOM (trong Vue) và lợi ích của việc sử dụng Virtual DOM thay vì Actual DOM, cùng với một số lưu ý khi sử dụng template. Cuối cùng mình có đề cập đến Render Function, một phần hay nhưng khá khó của Vue. Mình mong bài viết sẽ giúp ích cho các bạn một phần nào đó khi tìm hiểu về Vue.

References