Bài 10: Vòng đời Của Một Vue Instance Và Cách áp Dụng Vào Thực Tế

Mình đã cập nhật lại tất cả các bài với các thay đổi ở hiện tại ở năm 2024: Vue 3, Vite, Laravel 11x,...

Cập nhật gần nhất: 06/06/2024

Chào mừng các bạn quay trở lại với series học VueJS với Laravel của mình, ở bài trước mình đã hướng dẫn các bạn cách tạo component là truyền dữ liệu giữa chúng. Tiếp theo bài này chúng ta sẽ tìm hiểu về vòng đời của một Vue instance để có thể hiểu rõ hơn và sử dụng vào thực tế nhé.

Giới thiệu

Trong quá trình phát triển ứng dụng bạn sẽ thường xuyên phải quan tâm tới những việc kiểu như khi nào thì component được tạo, khi nào thì được thêm vào DOM, khi nào được render/re-render hay khi nào thì nó bị destroy. Bằng cách hiểu rõ hơn về vòng đời của Vue instance sẽ giúp chúng ta hiểu rõ được bản chất Vue hoạt động ra sau đằng sau vẻ ngoài hào nhoáng nhé 😄.

Chúng ta cùng xem qua bức ảnh "kinh điển" dưới đây (ảnh được lấy từ trang chủ Vue ):

lifecycle.MuZLBFAS.png

Giờ ta sẽ đi qua toàn bộ vòng đời 1 component VueJS xem nó có những gì nha 🚀🚀

setup()

Đây là một life cycle hook mới tinh được thêm vào từ version 3 🤩🤩, hook này được chạy đầu tiên và trước nhất, ở đó thường ta sẽ setup mọi thứ: data (ref, reactive), watcher, register handler khi các lifecycle hook khác được chạy.

Cái này chính là "trung tâm" cho Composition API mà ta vẫn hay nghe thấy ở đâu đó

<template> <div> <h1>{{ message }}</h1> </div> </template> <script setup> import { ref } from 'vue'; const message = ref('Hello, World!'); </script>

Đây là những thứ mà ta vẫn làm từ đầu series tới giờ luôn rồi, 😂

Ủa vậy là setup hook là cái script setup kia đó à?? 🤨🤨

Thực tế cái <script setup> đó là compile-time syntactic sugar thôi, hiểu đơn giản nó như kiểu là 1 cái "từ khoá", mà tại thời điểm compile Vue nó sẽ dựa vào cái đó và tạo ra một số code để có thể giúp ta giảm việc phải viết component dài dòng, cho performance tốt hơn, support Typescript,...

Quay lại ví dụ bên trên, thì nếu không có script setup thì code ta phải viết sẽ như sau:

<template> <div> <h1>{{ message }}</h1> <button @click="setMessage('Test')">click</button> </div> </template> <script> import { ref } from 'vue'; export default { setup(props, { expose }) { const message = ref('Hello, Vue 3!'); const setMessage = (newMessage) => { message.value = newMessage; }; return { message, setMessage }; }, } </script>

Như các bạn thấy thì không có script setup ta sẽ viết dạng export default đồng thời bất kì ref/reactive/methods,... ta sẽ phải mất công xử lý thêm nhiều, đôi khi ta còn quên không return xong lúc chạy lên thấy lỗi mới biết. Do vậy thì gần như 100% ta sẽ luôn dùng script setup cho ngắn gọn. Vue compiler lo hết 😎

Việc có compiler sẽ rất tiện cho framework trong việc generate code tối ưu hơn cho developer, giảm những công việc phải lặp đi lặp lại. Ví dụ hiện tại là 06/2024 thì React cuối cùng cũng đã đang thử nghiệm để phát hành Compiler của riêng 😉, chứ bắt ae developer tự nhớ useMemo mãi khổ lắm rồi 🤣. Vậy nên mình thấy Vue ngay từ đầu đã xác định có compiler của riêng nó để làm mọi thứ tối ưu nhất là một lựa chọn quá sáng suốt 😎

DOM, DOM ảo và Re-render là gì?

Trước khi đi tiếp ta học chút lý thuyết nha, cái này rất quan trọng và sẽ giúp các bạn hiểu hơn khi ta học tới những hook tiếp theo

Ở bên trên khi nãy mình có nhắc tới khái niệm DOM, và re-render. Chắc sẽ có nhiều bạn đã biết 2 khái niệm này là gì, nhưng mình nghĩ cũng có nhiều bạn vẫn còn mập mờ. Ở đây mình sẽ giải thích theo cách mình cho là dễ hiểu, chứ không dùng trực tiếp định nghĩa gốc vì nghe nó khá là khó hiểu 😄 (nếu có gì thắc mắc cứ comment bên dưới cho mình nhé)

DOM (Document Object Model): DOM là thứ thể hiện cho những dữ liệu và cấu trúc của toàn bộ trang HTML, những thứ bạn nhìn thấy ở trình duyệt: thẻ <a> ở chỗ này, thẻ <h1> đứng sau thẻ <h2> ở chỗ kia, chỗ này chữ phải màu vàng, chỗ này nội dung thẻ div là Hello World,...

DOM ảo (Virtual DOM): như các bạn đã biết Vue hay React được xây dựng với pattern là dùng DOM ảo. DOM ảo là một khái niệm lập trình mà ở đó những ý tưởng, design về UI của các bạn còn là "ảo", được lưu ở trong bộ nhớ 😄, và sẽ được đồng bộ với DOM thật (mình đã trình bày ở trên). Chú ý là những thứ ta nhìn thấy ở trình duyệt là DOM thật nhé các bạn 😉

Nghe lại hơi khó hiểu 😄 😄. Cho ví dụ cái coi

Ở Vue, DOM ảo các bạn có thể hiểu nó chính là phần template của chúng ta:

<template> <div>Hello world</div> </template>

Ở trên ta định nghĩa ra DOM ảo với dòng text Hello World, khi chạy Vue sẽ hiểu "à ông này muốn in ra thẻ div với dòng text kia", từ đó Vue sẽ đồng bộ với DOM thật và ta sẽ nhìn thấy ở trình duyệt

Re-render là gì: từ render tiếng anh dịch ra tiếng Việt có khá là nhiều nghĩa, nhưng ở trong HTML mình nghĩ phù hợp nhất ý nghĩa của nó là tô, vẽ. 1 lần render là 1 lần ta nhìn thấy nội dung được "tô, vẽ" hoàn chỉnh ở trên trình duyệt. Với Vue mỗi lần ta thay đổi 1 giá trị của 1 biến được khai báo ở data, hay computed thay đổi,... thì Vue sẽ render lại nội dung trên trình duyệt, quá trình render lại này ta gọi là re-render

Tốt nhất từ render các bạn đừng dịch ra tiếng Việt, cứ để nguyên tiếng anh nhé 😉

Qua phần giải thích này mong là các bạn đã hiểu hơn được về 1 vài khái niệm rất hay gặp khi làm việc với HTML, Vue hay React,..

onBeforeMount

Hook này được gọi trước khi component được mount vào DOM.

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onBeforeMount } from 'vue'; const message = ref('Hello, World!'); onBeforeMount(() => { console.log('Component is about to be mounted!'); }); </script>

Ở trên mình nói được gọi trước khi component được mount vào DOM, thế trước là như nào, như nào là trước 🤔

Ta sửa lại ví dụ trên như sau:

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onBeforeMount } from 'vue'; const message = ref('Hello, World!'); onBeforeMount(() => { console.log('Component is about to be mounted!'); console.log(document.getElementById('message').innerText); }); </script>

Khi chạy lên ta sẽ thấy báo lỗi ngay:

Screenshot 2024-06-06 at 10.13.48 PM.png

Lí do là vì ở bước này ta mới chỉ có DOM ảo được tạo ra, mà document.getElementById thì chỉ thao tác được với DOM thật, do đó nên nếu muốn dùng các thao tác với DOM thật thì ta cần làm ở mounted (khi DOM ảo đã được đồng bộ với DOM thật). Nhưng ở bước này ta đã có thể thao tác được với reactive state rồi, ví dụ gán data vào ref() chẳng hạn

Ứng dụng thực tế cho hook này là thường được dùng để gọi API lấy dữ liệu từ server, khởi tạo websocket, lắng nghe event Laravel Echo,... miễn là ta không động gì vào DOM thật là được.

Ví dụ fetch API:

<template> <div> <p v-if="loading">Loading...</p> <p v-else>{{ data }}</p> </div> </template> <script setup> import { ref, onBeforeMount } from 'vue'; const data = ref(null); const loading = ref(true); onBeforeMount(async () => { const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); data.value = await response.json(); loading.value = false; }); </script> onMounted

Hook này được gọi ngay sau khi component được mount vào DOM (component của ta đã thực sự có trên màn hình). Đây là nơi bạn nên thực hiện các thao tác cần truy cập đến DOM.

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onMounted } from 'vue'; const message = ref('Hello, World!'); onMounted(() => { console.log('Component has been mounted!'); console.log(document.getElementById('message').innerText); }); </script>

Ứng dụng thực tiễn:

  • Khởi tạo thư viện bên thứ ba như chart hoặc slider.
  • Dùng các thao tác với DOM thật: document.getElementById/document.querySelector, hoặc dùng JQuery

Ví dụ khởi tạo thư viện bên thứ ba (Chart.js):

<template> <div ref="chart"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue'; import Chart from 'chart.js'; const chart = ref(null); onMounted(() => { chart.value = new Chart(chart.value, { // options for the chart }); }); onBeforeUnmount(() => { if (chart.value) { chart.value.destroy(); } }); </script>

onMounted ở Vue giống như componentDidMount hoặc useEffect({...}, []) ở bên React

Lí do vì sao mình khuyên ta nên fetch API ở onBeforeMount thay vì onMounted là vì, API là thứ không liên quan tới component, nên ta có thể fetch càng sớm càng tốt, chuẩn bị sẵn data, khi component mounted thì data đã có sẵn ở đó rồi

onBeforeUpdate

Hook này được gọi trước khi component cập nhật lại do thay đổi reactive data (ví dụ khi ta update giá trị của ref()/reactive():

<template> <div> <p id="message">{{ message }}</p> <button @click="updateMessage">Update Message</button> </div> </template> <script setup> import { ref, onBeforeUpdate } from 'vue'; const message = ref('Hello, World!'); function updateMessage() { message.value = 'Hello, Vue 3!'; } onBeforeUpdate(() => { console.log('Component is about to be updated!'); console.log(document.getElementById('message').textContent); }); </script>

Trong ví dụ này, khi bạn nhấn nút "Update Message", giá trị của message thay đổi, và onBeforeUpdate sẽ được gọi trước khi DOM được cập nhật, ở thời điểm này Vue chưa cập nhật lại DOM thật, nên textContent của message vẫn là giá trị cũ

onUpdated

Hook này được gọi ngay sau khi component đã cập nhật DOM thật xong (re-render xong):

<template> <div> <p id="message">{{ message }}</p> <button @click="updateMessage">Update Message</button> </div> </template> <script setup> import { ref, onBeforeUpdate, onUpdated } from 'vue'; const message = ref('Hello, World!'); function updateMessage() { message.value = 'Hello, Vue 3!'; } onBeforeUpdate(() => { console.log('Component is about to be updated!'); console.log(document.getElementById('message').textContent); }); onUpdated(() => { console.log('Component updated!'); console.log(document.getElementById('message').textContent); }); </script>

Trong ví dụ này, khi message thay đổi và DOM được cập nhật, onUpdated sẽ được gọi ngay sau đó, chạy lên ta có thể thấy được textContent của thẻ <p id=message trước và sau khi update khác nhau:

Screenshot 2024-06-06 at 10.31.22 PM.png

Ứng dụng của onUpdated mình thấy thường là:

  • Đồng bộ hóa dữ liệu giữa DOM và các thư viện bên thứ ba
  • Thực hiện các thao tác sau khi DOM đã được cập nhật: ví dụ ta có danh sách message, thêm mới message -> DOM thật update -> scroll xuống dưới cuối
onBeforeUnmount

Hook này được gọi trước khi component bị hủy (unmount) khỏi DOM, ở ví dụ này ta có 2 component như sau:

<template> <Test v-if="isVisible" /> <button @click="toggle">Toggle</button> </template> <script setup> import Test from './TestComponent.vue'; import { ref } from 'vue'; const isVisible = ref(true); const toggle = () => { isVisible.value = !isVisible.value; }; </script>

và:

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onBeforeUnmount } from 'vue'; const message = ref('Hello, World!'); onBeforeUnmount(() => { console.log('Component is about to be unmounted!'); console.log(document.getElementById('message').innerText); }); </script>

Khi chạy lên và bấm nut Toggle ta thấy component Test sẽ bị unmount, và onBeforeUnmount sẽ được gọi, ở thời điểm này component vẫn hoạt động như bình thường không có gì bị mất và ta vẫn có thể truy cập vào DOM thật của component

Screenshot 2024-06-06 at 10.37.29 PM.png

onUnmounted

Hook này được gọi ngay sau khi component đã bị hủy khỏi DOM.

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onUnmounted } from 'vue'; const message = ref('Hello, World!'); onUnmounted(() => { console.log('Component has been unmounted!'); }); </script>

Ở ví dụ trên khi ta bấm nút sẽ thấy in ra dòng text Component has been unmounted! và nếu ta cố gắng truy cập vào DOM ở thời điểm này sẽ gặp lỗi

Screenshot 2024-06-06 at 10.43.27 PM.png

Ứng dụng thực tiễn:

  • Dọn dẹp tài nguyên như setTimeout/setInterval hoặc listener (ví dụ window.onscroll)
  • Hủy kết nối WebSocket hoặc API.
onErrorCaptured

Hook này được đặt ở component cha để bắt các lỗi mà component con gặp phải.

Ví dụ ta có 2 component:

<template> <Test v-if="isVisible" /> <button @click="toggle">Toggle</button> </template> <script setup> import Test from './TestComponent.vue'; import { ref, onErrorCaptured } from 'vue'; const isVisible = ref(true); const toggle = () => { isVisible.value = !isVisible.value; }; onErrorCaptured((error) => { console.error('An error occurred:', error); }); </script>

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onUnmounted } from 'vue'; const message = ref('Hello, World!'); onUnmounted(() => { console.log('Component has been unmounted!'); console.log(document.getElementById('message').innerText); }); </script>

Khi ta bấm nút, lỗi gặp phải từ TestComponent sẽ được ExampleComponent "bắt được" và in ra console.

Ví dụ thực tế của hook này là dùng làm ErrorBoundary, rất hữu ích, để đảm bảo app của chúng ta không bị crash và user không bao giờ phải thấy màn hình trắng 😝

onActivated

Hook này được gọi khi một component cached bởi <keep-alive> được kích hoạt và thêm vào DOM.

Ta có ví dụ sau:

<template> <KeepAlive> <Test /> </KeepAlive> </template> <script setup> import Test from './TestComponent.vue'; </script>

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onMounted, onActivated } from 'vue'; const message = ref('Hello, World!'); onMounted(() => { console.log('Component has been mounted!'); }); onActivated(() => { console.log('Component has been activated!'); }); </script>

Khi chạy lên ta sẽ thấy log báo ra console là component <Test> đã được activate, và như mình nói bên trên, nó xảy ra sau khi component được thêm vào DOM, nên nó sẽ được gọi sau onMounted:

Screenshot 2024-06-06 at 10.52.38 PM.png

onDeactivated

Hook này được gọi khi một component cached bởi <keep-alive> bị "hủy kích hoạt" và xoá khỏi DOM, ta có ví dụ sau:

<template> <KeepAlive> <Test v-if="isVisible" /> </KeepAlive> <button @click="toggle">Toggle</button> </template> <script setup> import Test from './TestComponent.vue'; import { ref } from 'vue'; const isVisible = ref(true); const toggle = () => { isVisible.value = !isVisible.value; }; </script>

<template> <div id="message">{{ message }}</div> </template> <script setup> import { ref, onMounted, onActivated, onUnmounted, onDeactivated } from 'vue'; const message = ref('Hello, World!'); onMounted(() => { console.log('Component has been mounted!'); }); onActivated(() => { console.log('Component has been activated!'); }); onUnmounted(() => { console.log('Component has been unmounted!'); }); onDeactivated(() => { console.log('Component has been deactivated!'); }); </script>

Khi ta bấm nút thì component sẽ bị deactivate, nếu ta inspect sẽ thấy ở DOM nó đã bị xoá đi, nhưng vì chỉ bị deactivate chứ chưa destroy nên hook onUnmounted không được gọi (các bạn chú ý cái này nhé):

Screenshot 2024-06-06 at 10.56.16 PM.png

Ứng dụng thực tiễn:

  • Lưu trạng thái component khi nó bị xoá khỏi DOM (deactivate)
  • Dọn dẹp các tài nguyên không cần thiết khi component không còn active.

Ví dụ cụ thể như bên dưới: ta có thể lưu state của 2 tab khi ta chuyển giữa các tab:

ezgif-2-7c50820386.gif

Các câu hỏi liên quan 📚

Tôi có thể dùng cùng 1 hook nhiều lần được không?

Trong Vue 3, bạn có thể sử dụng cùng một hook nhiều lần trong một component. Các hook này sẽ được gọi theo thứ tự mà chúng được khai báo.

<script setup> import { onMounted } from 'vue'; onMounted(() => { console.log('First onMounted hook'); }); onMounted(() => { console.log('Second onMounted hook'); }); </script>

Kết quả khi component được mount sẽ là:

First onMounted hook Second onMounted hook

Gọi hook ở trong một function được không?

Về cái này thì các bạn nhớ cho mình là Hook chỉ được gọi nếu ta để nó trực tiếp trong setup() hoặc dùng <script setup> hoặc gọi nó thông qua một function mà function đó được "bắt nguồn" từ setup(), và chú ý là nó phải được gọi đồng bộ (sync):

<template> <div> Hello World </div> <button @click="initHook">Init Hook</button> </template> <script> import { onMounted } from 'vue'; export default { setup() { console.log('This will work'); }, created() { onMounted(() => { console.log('OK, this will work too'); }); }, methods: { initHook() { onMounted(() => { console.log('This will not work'); }); }, }, }; </script>

Khi chạy lên nếu ta bấm vào nút Init hook sẽ có warning như sau:

Screenshot 2024-06-07 at 12.02.59 AM.png

Như ta thấy thì ta có thể để onMounted ở trong created cũng được vì created cũng được gọi "bắt nguồn" từ setup() nhưng không được gọi khi ta bấm nút và gọi method vì đây không còn là thao tác "đồng bộ" nữa rồi

Gọi hook ở trong setTimeout/setInterval hay sau khi "fetch" API được không?

Không, các lifecycle hooks phải được gọi trực tiếp trong setup() và không thể được gọi trong các hàm như setTimeout, setInterval, hoặc bên trong các promise như sau khi "fetch" API. Điều này là do các hooks cần được Vue xử lý tại thời điểm component được tạo một cách "đồng bộ" và không thể hoạt động chính xác nếu được gọi không đúng thời điểm.

<template> <div> Hello World </div> </template> <script setup> import { onMounted } from 'vue'; setTimeout(() => { onMounted(() => { console.log('This will not work'); }); }, 1000); fetch('https://jsonplaceholder.typicode.com/todos/1') .then(() => { onMounted(() => { console.log('This will also not work'); }); }); </script>

Khi chạy đoạn code trên ta sẽ thấy Vue báo warning như sau:

Screenshot 2024-06-06 at 11.55.18 PM.png

Kết luận

Qua bài này mong rằng các bạn có thể hiểu được về vòng đời của một component, qua đó sử dụng đúng đắn trong các ứng dụng/dự án thật của riêng các bạn. Đồng thời hiểu thêm một vài khái niệm như DOM, DOM ảo hay re-render mà rất hay được nhắc tới khi chúng ta làm việc với Vue hay React.

Ở bài tiếp theo chúng ta sẽ tìm hiểu về $forceUpdate để re-render lại DOM khi cần thiết nhé.

Cám ơn các bạn đã theo dõi, nếu có gì thắc mắc các bạn có thể comment bên dưới nhé ^^!

Từ khóa » Vòng đời Vue