Skip to content

Composables

Composable Là Gì?

Trong ngữ cảnh của ứng dụng Vue, một "composable" là một hàm sử dụng Composition API của Vue để đóng gói và tái sử dụng logic có trạng thái.

Khi xây dựng ứng dụng frontend, chúng ta thường cần tái sử dụng logic cho các công việc phổ biến. Ví dụ, chúng ta có thể cần định dạng các ngày ở nhiều nơi, vì vậy chúng ta trích xuất một hàm có thể tái sử dụng cho điều đó. Hàm định dạng này đóng gói logic không có trạng thái: nó nhận một số đầu vào và ngay lập tức trả về đầu ra mong đợi. Có nhiều thư viện có sẵn để tái sử dụng logic không có trạng thái - ví dụ như lodashdate-fns, mà bạn có thể đã nghe nói đến.

Ngược lại, logic có trạng thái liên quan đến việc quản lý trạng thái thay đổi theo thời gian. Một ví dụ đơn giản có thể là theo dõi vị trí hiện tại của con trỏ chuột trên trang. Trong các kịch bản thực tế, nó cũng có thể là logic phức tạp hơn như cử chỉ cảm ứng hoặc trạng thái kết nối đến cơ sở dữ liệu.

Ví Dụ Theo Dõi Chuột

Nếu chúng ta triển khai chức năng theo dõi chuột bằng cách sử dụng Composition API trực tiếp bên trong một thành phần, nó sẽ trông như sau:

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>Vị trí chuột ở: {{ x }}, {{ y }}</template>

Nhưng nếu chúng ta muốn tái sử dụng cùng một logic trong nhiều thành phần? Chúng ta có thể trích xuất logic vào một tệp ngoại cùng, dưới dạng một hàm composable:

js
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// theo quy ước, tên hàm composable bắt đầu bằng "use"
export function useMouse() {
  // trạng thái được đóng gói và quản lý bởi composable
  const x = ref(0)
  const y = ref(0)

  // một composable có thể cập nhật trạng thái quản lý của nó theo thời gian.
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // một composable cũng có thể kết nối vào vòng đời của thành phần chủ của nó
  // để thiết lập và hủy các hiệu ứng phụ.
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // tiết lộ trạng thái được quản lý dưới dạng giá trị trả về
  return { x, y }
}

Và đây là cách nó có thể được sử dụng trong các thành phần:

vue
<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Vị trí chuột ở: {{ x }}, {{ y }}</template>
Vị trí chuột ở: 0, 0

Thử nghiệm ở Playground

Như chúng ta có thể thấy, logic cốt lõi vẫn giống nhau - tất cả chúng ta cần phải làm là di chuyển nó vào một hàm bên ngoài và trả về trạng thái nên được tiết lộ. Giống như bên trong một thành phần, bạn có thể sử dụng toàn bộ loạt các chức năng Composition API trong composable. Chức năng useMouse() giờ đây có thể được sử dụng

trong bất kỳ thành phần nào.

Phần hay về composables là bạn cũng có thể lồng chúng: một hàm composable có thể gọi một hoặc nhiều hàm composable khác. Điều này cho phép chúng ta xây dựng logic phức tạp bằng cách sử dụng các đơn vị nhỏ, cô lập, tương tự như cách chúng ta xây dựng một ứng dụng hoàn chỉnh bằng cách sử dụng các thành phần. Trên thực tế, đây là lý do tại sao chúng tôi quyết định gọi bộ API tập hợp là Composition API.

Ví dụ, chúng ta có thể trích xuất logic thêm và xóa nghe sự kiện DOM vào composable riêng của mình:

js
// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  // nếu bạn muốn, bạn cũng có thể làm cho điều này
  // hỗ trợ chuỗi lựa chọn như target
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

Và bây giờ composable useMouse() của chúng ta có thể được đơn giản hóa thành:

js
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  useEventListener(window, 'mousemove', (event) => {
    x.value = event.pageX
    y.value = event.pageY
  })

  return { x, y }
}

TIP

Mỗi phiên bản thành phần gọi useMouse() sẽ tạo ra bản sao riêng của trạng thái xy, vì vậy chúng sẽ không giao cắt lẫn nhau. Nếu bạn muốn quản lý trạng thái được chia sẻ giữa các thành phần, hãy đọc Chương Quản Lý Trạng Thái.

Ví Dụ Về Trạng Thái Async

Hàm composable useMouse() không nhận bất kỳ đối số nào, vì vậy hãy xem một ví dụ khác sử dụng một đối số. Khi thực hiện việc truy xuất dữ liệu bất kỳ, chúng ta thường cần xử lý các trạng thái khác nhau: đang tải, thành công và lỗi:

vue
<script setup>
import { ref } from 'vue'

const data = ref(null)
const error = ref(null)

fetch('...')
  .then((res) => res.json())
  .then((json) => (data.value = json))
  .catch((err) => (error.value = err))
</script>

<template>
  <div v-if="error">Oops! Gặp lỗi: {{ error.message }}</div>
  <div v-else-if="data">
    Dữ liệu đã được tải:
    <pre>{{ data }}</pre>
  </div>
  <div v-else>Đang tải...</div>
</template>

Việc phải lặp lại mẫu này trong mỗi thành phần cần truy xuất dữ liệu sẽ làm cho mã trở nên khó quản lý. Hãy trích xuất nó thành một hàm composable:

js
// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err))

  return { data, error }
}

Bây giờ trong thành phần của chúng ta, chúng ta chỉ cần làm như sau:

vue
<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

Chấp Nhận Trạng Thái Reactive

useFetch() nhận một chuỗi URL tĩnh làm đầu vào - vì vậy nó thực hiện truy xuất chỉ một lần và sau đó kết thúc. Nhưng nếu chúng ta muốn nó truy xuất lại mỗi khi URL thay đổi, chúng ta cần truyền trạng thái reactive vào hàm composable và để hàm composable tạo ra các watchers thực hiện hành động bằng cách sử dụng trạng thái đã được truyền vào.

Ví dụ, useFetch() nên có khả năng chấp nhận một ref:

js
const url = ref('/initial-url')

const { data, error } = useFetch(url)

// điều này sẽ kích hoạt lại việc truy xuất
url.value = '/new-url'

Hoặc, chấp nhận một hàm getter:

js
// truy xuất lại khi props.id thay đổi
const { data, error } = useFetch(() => `/posts/${props.id}`)

Chúng ta có thể tái cấu trúc phiên bản hiện tại của chúng ta bằng cách sử dụng các API watchEffect()toValue():

js
// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    // đặt lại trạng thái trước khi truy xuất..
    data.value = null
    error.value = null

    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error }
}

toValue() là một API được thêm vào từ phiên bản 3.3. Nó được thiết kế để chuẩn hóa refs hoặc getters thành giá trị. Nếu đối số là một ref, nó trả về giá trị của ref; nếu đối số là một hàm, nó sẽ gọi hàm và trả về giá trị trả về của nó. Nếu không, nó trả về đối số như đã truyền vào. Nó hoạt động tương tự như unref(), nhưng với xử lý đặc biệt cho các hàm.

Chú ý rằng toValue(url) được gọi bên trong hàm callback của watchEffect. Điều này đảm bảo rằng bất kỳ phụ thuộc reactive nào được truy cập trong quá trình chuẩn hóa toValue() đều được theo dõi bởi watcher.

Phiên bản này của useFetch() bây giờ có thể chấp nhận chuỗi URL tĩnh, refs và getters, làm cho nó linh hoạt hơn nhiều. Hiệu ứng theo dõi sẽ chạy ngay lập tức và sẽ theo dõi bất kỳ phụ thuộc nào được truy cập trong quá trình toValue(url). Nếu không có phụ thuộc nào được theo dõi (ví dụ: url đã là một chuỗi), hiệu ứng sẽ chỉ chạy một lần; ngược lại, nó sẽ chạy lại mỗi khi một phụ thuộc theo dõi thay đổi.

Dưới đây là phiên bản cập nhật của useFetch(), với độ trễ nhân tạo và lỗi ngẫu nhiên cho mục đích demo.

Quy ước và Thực Pratice Tốt

Đặt Tên

Là quy ước để đặt tên các hàm composable bằng các tên camelCase và bắt đầu bằng "use".

Đối Số Đầu Vào

Một hàm composable có thể chấp nhận đối số là ref hoặc getter ngay cả khi nó không phụ thuộc vào chúng để tạo ra tính phản ứng. Nếu bạn đang viết một hàm composable có thể được sử dụng bởi các nhà phát triển khác, đây là một ý tưởng tốt để xử lý trường hợp đối số đầu vào là refs hoặc getters thay vì là giá trị nguyên thủy. Hàm tiện ích toValue() sẽ hữu ích cho mục đích này:

js
import { toValue } from 'vue'

function useFeature(maybeRefOrGetter) {
  // Nếu maybeRefOrGetter là ref hoặc getter,
  // giá trị đã chuẩn hóa của nó sẽ được trả về.
  // Ngược lại, nó sẽ được trả về như đã truyền vào.
  const value = toValue(maybeRefOrGetter)
}

Nếu hàm composable của bạn tạo ra các hiệu ứng phản ứng khi đầu vào là một ref hoặc getter, hãy chắc chắn là hoặc rõ ràng theo dõi ref/getter với watch(), hoặc gọi toValue() bên trong một watchEffect() để nó được theo dõi đúng cách.

Việc sử dụng đoạn mã useFetch() được thảo luận trước đó cung cấp một ví dụ cụ thể về một hàm composable chấp nhận refs, getters và giá trị thông thường như đối số đầu vào.

Giá Trị Trả Về

Bạn có thể đã chú ý rằng chúng ta đã luôn sử dụng ref() thay vì reactive() trong các hàm composable. Quy ước được khuyến khích là để các hàm composable luôn trả về một đối tượng bình thường, không phản ứng chứa nhiều refs. Điều này cho phép nó được giải cấu trúc trong các thành phần trong khi vẫn giữ tính phản ứng:

js
// x và y là refs
const { x, y } = useMouse()

Việc trả về một đối tượng phản ứng từ một hàm composable sẽ khiến cho việc giải cấu trúc như vậy mất kết nối với tính phản ứng của trạng thái bên trong hàm composable, trong khi refs sẽ giữ lại kết nối đó.

Nếu bạn muốn sử dụng trạng thái được trả về từ các hàm composable như là các thuộc tính của đối tượng, bạn có thể bao bọc đối tượng được trả về bằng reactive() để refs được bóc giải. Ví dụ:

js
const mouse = reactive(useMouse())
// mouse.x liên kết với ref gốc
console.log(mouse.x)
template
Vị trí chuột là ở: {{ mouse.x }}, {{ mouse.y }}

Hiệu Ứng Phụ

Hoàn toàn có thể thực hiện các hiệu ứng phụ (ví dụ: thêm bộ lắng nghe sự kiện DOM hoặc truy xuất dữ liệu) trong các hàm composable, nhưng hãy chú ý đến các quy tắc sau:

  • Nếu bạn đang làm việc trên ứng dụng sử dụng Server-Side Rendering (SSR), hãy đảm bảo thực hiện các hiệu ứng phụ cụ thể của DOM trong các hooks vòng đời sau khi lắp đặt, ví dụ: onMounted(). Những hooks này chỉ được gọi trong trình duyệt, vì vậy bạn có thể đảm bảo rằng mã bên trong chúng có quyền truy cập vào DOM.

  • Hãy nhớ làm sạch các hiệu ứng phụ trong onUnmounted(). Ví dụ, nếu một hàm composable thiết lập một bộ lắng nghe sự kiện DOM, nó nên gỡ bỏ bộ lắng nghe đó trong onUnmounted() như chúng ta đã thấy trong ví dụ của useMouse(). Có thể là một ý tưởng tốt là sử dụng một hàm composable tự động thực hiện điều này cho bạn, như ví dụ useEventListener().

Hạn Chế Sử Dụng

Hàm composable chỉ nên được gọi trong <script setup> hoặc hook setup(). Chúng cũng nên được gọi đồng bộ trong các ngữ cảnh này. Trong một số trường hợp, bạn cũng có thể gọi chúng trong các hook vòng đời như onMounted().

Những hạn chế này quan trọng vì đây là các ngữ cảnh mà Vue có thể xác định được ví dụ cụ cụ thể hiện tại của thành phần. Việc truy cập vào một ví dụ cụ thể thành phần hiện tại là cần thiết để:

  1. Các hooks vòng đời có thể được đăng ký với nó.

  2. Các tính toán và bộ theo dõi có thể được liên kết với nó, để chúng có thể bị huỷ khi ví dụ được gỡ bỏ để ngăn rò rỉ bộ nhớ.

TIP

<script setup> là nơi duy nhất bạn có thể gọi các hàm composable sau khi sử dụng từ khóa await. Trình biên dịch tự động khôi phục ngữ cảnh ví dụ hoạt động cho bạn sau khi thực hiện hoạt động bất đồng bộ.

Trích Xuất Composables để Tổ Chức Mã

Composables không chỉ có thể được trích xuất để tái sử dụng mà còn để tổ chức mã. Khi độ phức tạp của các thành phần của bạn tăng lên, bạn có thể kết thúc với các thành phần quá lớn để điều hướng và suy luận. Composition API mang lại cho bạn sự linh hoạt hoàn chỉnh để tổ chức mã thành các hàm nhỏ dựa trên các vấn đề logic:

vue
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'

const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

Một cách nhìn về những composables đã được trích xuất như là các dịch vụ được giới hạn trong phạm vi của thành phần có thể nói chuyện với nhau một cách tương tác.

Sử Dụng Composables trong Options API

Nếu bạn đang sử dụng Options API, composables phải được gọi bên trong setup(), và các giá trị trả về phải được trả về từ setup() để chúng được tiết lộ cho this và template:

js
import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    const { x, y } = useMouse()
    const { data, error } = useFetch('...')
    return { x, y, data, error }
  },
  mounted() {
    // Các thuộc tính được tiết lộ từ setup() có thể được truy cập thông qua `this`
    console.log(this.x)
  }
  // ...các tùy chọn khác
}

So sánh với Các Kỹ Thuật Khác

So sánh với Mixins

Người dùng chuyển từ Vue 2 có thể quen với tùy chọn mixins, cũng cho phép chúng ta trích xuất logic thành phần thành các đơn vị có thể tái sử dụng. Có ba điểm chính mà mixins gặp khó khăn:

  1. Nguồn rõ ràng của các thuộc tính: khi sử dụng nhiều mixins, trở nên không rõ ràng thuộc tính của đối tượng nào được chèn bởi mixin nào, làm cho việc theo dõi cài đặt và hiểu về hành vi của thành phần trở nên khó khăn. Đây cũng là lý do tại sao chúng ta khuyến nghị sử dụng mẫu refs + destructure cho composables: nó giúp làm rõ nguồn của thuộc tính trong các thành phần sử dụng.

  2. Xung đột không gian tên: nhiều mixins từ các tác giả khác nhau có thể đăng ký các khóa thuộc tính giống nhau, gây ra xung đột không gian tên. Với composables, bạn có thể đổi tên các biến được giải nén nếu có các khóa xung đột từ các composables khác nhau.

  3. Giao tiếp chéo mixin ngầm: nhiều mixins cần tương tác với nhau phải dựa vào các khóa thuộc tính chia sẻ, làm cho chúng liên kết một cách ngầm định. Với composables, giá trị trả về từ một composable có thể được truyền vào một composable khác dưới dạng đối số, giống như các hàm bình thường.

Vì các lý do trên, chúng tôi không khuyến khích sử dụng mixins trong Vue 3 nữa. Tính năng này chỉ được giữ lại vì lý do chuyển đổi và quen thuộc.

So sánh với Các Component không Vẽ (Renderless Components)

Trong chương về các khe cắm component, chúng ta đã thảo luận về mô hình Renderless Component dựa trên khe cắm scoped. Chúng ta thậm chí đã triển khai cùng một demo theo dõi chuột bằng cách sử dụng các component không vẽ.

Ưu điểm chính của composables so với component không vẽ là composables không tạo thêm chi phí của đối tượng component. Khi sử dụng trên toàn bộ ứng dụng, lượng đối tượng component thêm vào bằng mô hình component không vẽ có thể trở thành một chi phí hiệu suất đáng chú ý.

Đề xuất sử dụng composables khi tái sử dụng logic thuần túy và sử dụng component khi tái sử dụng cả logic và bố cục hình thức.

So sánh với React Hooks

Nếu bạn có kinh nghiệm với React, bạn có thể nhận ra rằng điều này trông rất giống với các React hooks tùy chỉnh. Composition API được tạo ra một phần dựa trên React hooks, và Vue composables thực sự tương tự với React hooks về khả năng sắp xếp logic. Tuy nhiên, Vue composables dựa trên hệ thống phản ứng tinh tế của Vue, điều này khác biệt cơ bản so với mô hình thực thi của React hooks. Điều này được thảo luận chi tiết hơn trong Composition API FAQ.

Composables has loaded