Bài 10: Vòng đời của một Vue instance và cách áp dụng vào thực tế

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 bài này mình lấy nhiều ý tưởng từ bài post này vì mình thấy nó khá là ổn, mình sẽ thêm ví dụ thực tế mình áp dụng để các bạn hiểu rõ hơn nhé.

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 cập nhật hay khi nào thì nó bị huỷ đi. 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 ):

Vue_lifecycle

Creating

Quá trình khởi tạo là quá trình diễn ra trước nhất trên component, ở quá trình này chúng ta có thể thực hiện các hành động với dữ liệu của component trước khi chúng thực sự được thêm vào DOM. Trong quá trình này chúng ta có thể thiết lập các thông số, thực hiện thao tác lấy dữ liệu trước khi component được đưa vào DOM.

Ví dụ như mình hay làm các thao tác lấy dữ liệu từ backend bên Laravel ở giai đoạn này (sử dụng created). Các thao tác lên các phần tử trong DOM lúc này sẽ không thực hiện được, điều đó lý giải cho việc chúng ta dùng JQuery trong created và bị báo lỗi $ is not defined.

Sử dụng ví dụ từ các bài trước, ở bài này chúng ta tạo mới một file tên là LifeCycle.vue, sau đó các bạn khai báo nó trong app.jswelcome.blade.php. Nhớ luôn chạy php artisan servenpm run watch nhé:

<template>
    <div id="my-text">
        This is my text
    </div>
</template>

<script>
    export default {

    }
</script>

<style lang="scss" scoped>
</style>

beforeCreate

Hàm beforeCreate(mình sử dụng từ này cho các bạn dễ hình dung, chuẩn hơn nên dùng là hook) chạy ngay trước quá trình mà một component được khởi tạo. Trong quá trình này các data mà chúng ta khai báo chưa reactive (tự thay đổi khi có cập nhật) đồng thời các events cũng chưa được khởi tạo.

<template>
    <div id="my-text">
        This is my text
    </div>
</template>

<script>
    export default {
        data() {
            return {
                message: 'Hello World'
            }
        },
        beforeCreate() {
            console.log(this.message)
        }
    }
</script>

<style lang="scss" scoped>
</style>

Ví dụ trên khi các bạn chạy sẽ thấy báo lỗi undefined vì lúc này ta chưa thể tương tác với dữ liệu trong data

created

Ngay khi component được tạo, hàm created có thể được sử dụng để thao tác với các dữ liệu trong data và các sự kiện mà các bạn thiết lập đã có thể được kích hoạt. Nhưng template và DOM ảo chưa được mountrender, tức là nếu các bạn truy cập đến các phần tử trong DOM lúc này sẽ không được và báo lỗi. Chúng ta sửa lại file ví dụ như sau:

<template>
    <div id="my-text">
        This is my text
    </div>
</template>

<script>
    export default {
        data() {
            return {
                message: 'Hello World'
            }
        },
        created() {
            console.log(this.message)

            console.log(document.getElementById('my-text').innerHTML)
        }
    }
</script>

<style lang="scss" scoped>
</style>

Khi chạy sẽ báo lỗi sau:

error_created_Vue

Chỉ có dòng in ra message trong data được thực hiện, còn việc truy cập vào phần tử my-text trên template sẽ báo lỗi. Mình hay sử dụng created để fetch data từ phía backend ngay lúc khởi tạo.

Mounting

Quá trình mounting xảy ra ngay trước và sau khi component của các bạn được khởi tạo. Thường được sử dụng khi các bạn cần truy cập vào các phần tử trong DOM, ví dụ điển hình là chúng ta đã có thể dùng JQuery tại quá trình này. Và không nên sử dụng nếu các bạn muốn fetch data cho các thiết lập ban bởi vì mounting chỉ chạy trong quá trình client side rendering nên nếu các bạn sử dụng server side rendering sẽ báo lỗi, created thì có thể chạy trên cả 2 loại.

Bài viết về server và client side rendering các bạn có thể xem ở đây

beforeMount

beforeMount được gọi sau khi component đã được compile và trước lần render đầu tiên. Ở giai đoạn này khi các bạn truy cập đến các phần tử trong DOM vẫn sẽ báo lỗi:

<template>
    <div id="my-text">
        This is my text
    </div>
</template>

<script>
    export default {
        beforeMount() {
            console.log(this)
            console.log(document.getElementById('my-text').innerHTML)
         }
    }
</script>

<style lang="scss" scoped>
</style>

Ở đây khi chạy các bạn có thể thấy đã có thể in ra this là component hiện tại nhưng truy cập vào Virtual DOM thì vẫn báo lỗi null

mounted

Ở quá trình này chúng ta đã có đầy đủ quyền truy cập vào data, template, DOM (bằng cách gọi this.$el). Mình thường dùng mounted khi dùng chung với Jquery để tác động vào các phần tử DOM, như ở ví dụ bên dưới ta đã có thể truy cập vào các phần tử trong DOM:

<template>
    <div id="my-text">
        This is my text
    </div>
</template>

<script>
    export default {
        mounted() {
            console.log(this.$el)
            console.log(document.getElementById('my-text').innerHTML)
         }
    }
</script>

<style lang="scss" scoped>
</style>

Updating

Quá trình này được gọi bất cứ khi nào các dữ liệu reactive bị thay đổi hoặc ta tác động khiến cho component phải re-render.

Các hàm sử dụng trong quá trình này thường sử dụng trong debug xem khi nào component re-render, còn nếu các bạn muốn debug với các dữ liệu reactive thì nên dùng computed hoặc watch

beforeUpdate

Quá trình này được gọi ngay sau khi dữ liệu trên component bị thay đổi và trước khi component re-render, ví dụ bên dưới sẽ log ra màn hình liên tục các giá trị của counter sau khi nó bị thay đổi và trước khi DOM được re-render:

<template>
    <div id="my-text">
        {{ counter }}
    </div>
</template>

<script>
    export default {
      data() {
        return {
          counter: 0
        }
      },
      beforeUpdate() {
        console.log(this.counter) // Logs the counter value every second, before the DOM updates.
      },
      created() {
        setInterval(() => {
          this.counter++
        }, 1000)
      }
}
</script>

<style lang="scss" scoped>
</style>

Chú ý: Ở đầu bài post mình có nói là bài này mình lấy ý tưởng từ một bài post khác (link mình có đính kèm trên đầu), nhưng ở phần này họ chỉ đưa ra code phần script, khi chạy thì ta không thấy log ra console gì cả.

Mấu chốt là ta không render nó mà mới chỉ thay đổi nó nên mỗi khi thay đổi dữ liệu ta không re-render cái gì cả nên hàm beforeUpdate không được gọi.

updated

Quá trình này xảy ra sau beforeUpdate, ở đây DOM đã được cập nhật lại, ở ví dụ dưới ta có thể so sánh giá trị ngay sau khi ta set ở mỗi vòng interval và giá trị sau khi updated đều bằng nhau, chứng ta DOM đã được re-render thành công:

<template>
    <p ref="dom-element">{{counter}}</p>
</template>
<script>
export default {
    data() {
        return {
          counter: 0
        }
    },
    updated() {
        // Fired every second, should always be true
        console.log(+this.$refs['dom-element'].textContent === this.counter)
    },
    created() {
        setInterval(() => {
          this.counter++
        }, 1000)
    }
}
</script>

<style lang="scss" scoped>
</style>

Destroy

beforeDestroy

Quá trình này xay ra ngay trước khi component của chúng ta bị huỷ đi (ví dụ như lúc chúng ta chuyển từ component này sang component khác, hay như lúc ta chuyển route,...). Tại đây component vẫn còn đầy đủ những yếu tố như data, events,... Ta thường dùng hàm này để xoá đi các sự kiện không cần thiết sau khi component bị huỷ

Với mình đó là khi mình làm chức năng chat realtime, khi khởi tạo component mình sẽ lắng nghe sự kiện khi có một người dùng gửi tin nhắn đến sẽ có tiếng "Bíp", nếu chúng ta để nguyên thì khi chuyển route, qua component khác, khi có người dùng nhắn tin đến vẫn thấy tiếng kêu "Bíp", bởi vì mình vẫn đang tiếp tục lắng nghe sự kiện, do đó mình đã phải dùng tới beforeDestroy để huỷ lắng nghe khi chuyển trang.

Ví dụ phần này thì cũng không có gì khác biệt nên các bạn có thể tự làm nhé.

destroyed

Tại quá trình này thì hầu như mọi thứ trên component đã bị huỷ đi (data, events,...), nhưng ta vẫn có thể làm một số việc như thông báo với remote server là component vừa bị huỷ chẳng hạn.

activate và deactivate

Ngoài ra Vue còn có 2 quá trình nữa là activatedeactivate các bạn có thể xem trên trang chủ của Vue nhé.

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. Với cá nhân thì mình hầu như chỉ sử dụng createdmounted trong khi phát triển dự án, tuỳ vào mục đích cá nhân 😃

Ở 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é ^^!