Chào mừng đến với Cafedev! Trong thế giới phát triển web hiện đại, hiểu biết về khả năng phản ứng của dữ liệu là rất quan trọng. Chính vì vậy, hôm nay chúng ta sẽ khám phá về Vuejs với Cơ bản về Phản ứng. Vuejs không chỉ là một framework mạnh mẽ cho việc xây dựng ứng dụng web, mà còn cung cấp các công cụ mạnh mẽ để quản lý và phản ứng với dữ liệu. Hãy cùng nhau khám phá thêm về cơ bản của tính năng phản ứng trong Vuejs!

tip Ưu tiên API Trang này và nhiều chương sau trong hướng dẫn chứa nội dung khác nhau cho API Options và API Composition. Ưu tiên hiện tại của bạn là API OptionsAPI Composition. Bạn có thể chuyển đổi giữa các kiểu API bằng cách sử dụng các nút “Ưu tiên API” ở đầu thanh bên trái.

1. Khai báo Trạng thái Phản ứng

Với API Options, chúng ta sử dụng tùy chọn data để khai báo trạng thái phản ứng của một thành phần. Giá trị tùy chọn nên là một hàm trả về một đối tượng. Vue sẽ gọi hàm khi tạo một phiên bản thành phần mới, và bọc đối tượng được trả về trong hệ thống phản ứng của nó. Bất kỳ thuộc tính cấp cao của đối tượng này đều được proxy trên phiên bản thành phần (this trong các phương thức và hooks vòng đời):


export default {
  data() {
    return {
      count: 1
    }
  },

  // `mounted` is a lifecycle hook which we will explain later
  mounted() {
    // `this` refers to the component instance.
    console.log(this.count) // => 1

    // data can be mutated as well
    this.count = 2
  }
}

Thử nghiệm tại Playground

Những thuộc tính phiên bản này chỉ được thêm khi phiên bản được tạo lần đầu tiên, vì vậy bạn cần đảm bảo rằng chúng đều có mặt trong đối tượng được trả về bởi hàm data. Khi cần thiết, sử dụng null, undefined hoặc một giá trị giữ chỗ khác cho các thuộc tính mà giá trị mong muốn chưa có sẵn.
Có thể thêm một thuộc tính mới trực tiếp vào this mà không cần phải bao gồm nó trong data. Tuy nhiên, các thuộc tính được thêm theo cách này sẽ không thể kích hoạt các cập nhật phản ứng.

Vue sử dụng tiền tố $ khi tiết lộ các API tích hợp sẵn của nó qua phiên bản thành phần. Nó cũng dành tiền tố _ cho các thuộc tính nội bộ. Bạn nên tránh sử dụng tên cho các thuộc tính data cấp cao bắt đầu bằng một trong hai ký tự này.

1.1 Proxy Phản ứng so với Gốc *

Trong Vue 3, dữ liệu được làm phản ứng bằng cách tận dụng Proxy JavaScript. Người dùng từ Vue 2 nên nhận biết trường hợp biên như sau:

export default {
  data() {
    return {
      someObject: {}
    }
  },
  mounted() {
    const newObject = {}
    this.someObject = newObject

    console.log(newObject === this.someObject) // false
  }
}

Khi bạn truy cập this.someObject sau khi gán nó, giá trị là một proxy phản ứng của newObject gốc. Khác với Vue 2, newObject gốc được giữ nguyên và sẽ không được làm phản ứng: hãy luôn truy cập trạng thái phản ứng dưới dạng thuộc tính của this.

2. Khai báo Trạng thái Phản ứng **

2.1 ref() **

Trong API Composition, cách khuyến nghị để khai báo trạng thái phản ứng là sử dụng hàm ref():

import { ref } from 'vue'

const count = ref(0)

ref() nhận đối số và trả về nó được bao bọc trong một đối tượng ref với thuộc tính .value:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

Xem thêm: Kiểu Refs
Để truy cập refs trong mẫu của một thành phần, khai báo và trả về chúng từ hàm setup() của một thành phần:

{5,9-11}
import { ref } from 'vue'

export default {
  // `setup` is a special hook dedicated for the Composition API.
  setup() {
    const count = ref(0)

    // expose the ref to the template
    return {
      count
    }
  }
}
<div>{{ count }}</div>

Lưu ý rằng chúng ta không cần phải thêm .value khi sử dụng ref trong mẫu. Để thuận tiện, refs tự động được giải gói khi sử dụng trong các mẫu (với một số điều cần lưu ý).
Bạn cũng có thể thay đổi một ref trực tiếp trong các xử lý sự kiện:


<button @click="count++">
  {{ count }}
</button>

Đối với logic phức tạp hơn, chúng ta có thể khai báo các hàm thay đổi refs trong cùng phạm vi và tiết lộ chúng như các phương thức cùng với trạng thái:


import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      // .value is needed in JavaScript
      count.value++
    }

    // don't forget to expose the function as well.
    return {
      count,
      increment
    }
  }
}

Các phương thức tiết lộ sau đó có thể được sử dụng như các xử lý sự kiện:


<button @click="increment">
  {{ count }}
</button>

Dưới đây là ví dụ trực tiếp trên Codepen, mà không sử dụng bất kỳ công cụ xây dựng nào.


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

const count = ref(0)

function increment() {
  count.value++
}
</script>

  <button @click="increment">
    {{ count }}
  </button>

Thử nghiệm tại Playground
Nếu bạn không sử dụng SFC, bạn vẫn có thể sử dụng API Composition với tùy chọn setup().

2.2 Tại sao Refs? **

Bạn có thể đang tự hỏi tại sao chúng ta cần refs với .value thay vì biến đơn giản. Để giải thích điều đó, chúng ta sẽ cần thảo luận ngắn gọn về cách hệ thống phản ứng của Vue hoạt động.
Khi bạn sử dụng một ref trong một mẫu, và sau đó thay đổi giá trị của ref, Vue tự động phát hiện sự thay đổi và cập nhật DOM tương ứng. Điều này là có thể nhờ vào hệ thống phản ứng dựa trên việc theo dõi các phụ thuộc. Khi một thành phần được hiển thị lần đầu tiên, Vue theo dõi mọi ref đã được sử dụng trong quá trình hiển thị. Sau này, khi một ref được thay đổi, nó sẽ kích hoạt một lần hiển thị lại cho các thành phần đang theo dõi nó.

Trong JavaScript tiêu chuẩn, không có cách nào để phát hiện việc truy cập hoặc thay đổi của biến đơn giản. Tuy nhiên, chúng ta có thể ngăn chặn các hoạt động get và set của các thuộc tính của một đối tượng bằng cách sử dụng các phương thức getter và setter.

Thuộc tính .value cho phép Vue phát hiện khi một ref đã được truy cập hoặc thay đổi. Bên dưới, Vue thực hiện theo dõi trong getter của nó và kích hoạt trong setter của nó. Về mặt khái niệm, bạn có thể nghĩ về một ref như một đối tượng có dạng như sau:

// pseudo code, not actual implementation
const myRef = {
  _value: 0,
  get value() {
    track()
    return this._value
  },
  set value(newValue) {
    this._value = newValue
    trigger()
  }
}

Một điểm mạnh khác của refs là khác với biến đơn giản, bạn có thể truyền refs vào các hàm trong khi vẫn giữ được quyền truy cập vào giá trị mới nhất và kết nối phản ứng. Điều này đặc biệt hữu ích khi tái cấu trúc logic phức tạp thành mã có thể tái sử dụng.

3. Khai báo Phương thức *


Để thêm phương thức vào một thể hiện thành phần, chúng ta sử dụng tùy chọn methods. Điều này nên là một đối tượng chứa các phương thức mong muốn:


export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // methods can be called in lifecycle hooks, or other methods!
    this.increment()
  }
}

Vue tự động ràng buộc giá trị this cho methods để luôn tham chiếu đến thể hiện thành phần. Điều này đảm bảo rằng một phương thức giữ giá trị this chính xác nếu nó được sử dụng như một trình nghe sự kiện hoặc gọi lại. Bạn nên tránh sử dụng các hàm mũi tên khi định nghĩa methods, vì điều đó ngăn Vue khỏi việc ràng buộc giá trị this thích hợp:

export default {
  methods: {
    increment: () => {
      // BAD: no `this` access here!
    }
  }
}

Giống như tất cả các thuộc tính khác của thể hiện thành phần, các methods có thể truy cập từ bên trong mẫu của thành phần. Trong một mẫu, chúng thường được sử dụng như các trình nghe sự kiện:

<button @click="increment">{{ count }}</button>

Thử nghiệm ở Playground

3.1 Phản ứng Sâu

Trong Vue, trạng thái mặc định là phản ứng sâu. Điều này có nghĩa là bạn có thể mong đợi các thay đổi được phát hiện ngay cả khi bạn thay đổi các đối tượng hoặc mảng lồng nhau:

export default {
  data() {
    return {
      obj: {
        nested: { count: 0 },
        arr: ['foo', 'bar']
      }
    }
  },
  methods: {
    mutateDeeply() {
      // these will work as expected.
      this.obj.nested.count++
      this.obj.arr.push('baz')
    }
  }
}

Refs có thể giữ bất kỳ loại giá trị nào, bao gồm các đối tượng lồng nhau sâu, mảng hoặc các cấu trúc dữ liệu tích hợp của JavaScript như Map.
Một ref sẽ làm cho giá trị của nó phản ứng sâu. Điều này có nghĩa là bạn có thể mong đợi các thay đổi được phát hiện ngay cả khi bạn thay đổi các đối tượng hoặc mảng lồng nhau:

import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // these will work as expected.
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

Các giá trị không nguyên thủy được chuyển đổi thành bộ lọc phản ứng thông qua reactive(), được thảo luận bên dưới.
Cũng có thể chọn không tham gia vào phản ứng sâu với refs nông. Đối với refs nông, chỉ việc truy cập .value được theo dõi cho phản ứng. Refs nông có thể được sử dụng để tối ưu hóa hiệu suất bằng cách tránh chi phí quan sát của các đối tượng lớn, hoặc trong các trường hợp mà trạng thái bên trong được quản lý bởi một thư viện bên ngoài.

3.2 Thời gian Cập nhật DOM

Khi bạn thay đổi trạng thái phản ứng, DOM sẽ được cập nhật tự động. Tuy nhiên, cần lưu ý rằng các cập nhật DOM không được áp dụng đồng bộ. Thay vào đó, Vue sẽ lưu trữ chúng cho đến “tick tiếp theo” trong chu kỳ cập nhật để đảm bảo mỗi thành phần chỉ cập nhật một lần dù có bao nhiêu thay đổi trạng thái bạn đã thực hiện.
Để đợi cập nhật DOM hoàn thành sau khi thay đổi trạng thái, bạn có thể sử dụng API toàn cục nextTick():

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // Now the DOM is updated
}
import { nextTick } from 'vue'

export default {
  methods: {
    async increment() {
      this.count++
      await nextTick()
      // Now the DOM is updated
    }
  }
}

4. reactive() **

Có một cách khác để khai báo trạng thái phản ứng, với API reactive(). Khác với ref là bọc giá trị bên trong trong một đối tượng đặc biệt, reactive() làm cho một đối tượng chính nó phản ứng:

import { reactive } from 'vue'

const state = reactive({ count: 0 })

Xem thêm: Định nghĩa kiểu Phản ứng
Sử dụng trong template:

<button @click="state.count++">
  {{ state.count }}
</button>

Các đối tượng phản ứng là Proxy JavaScript và hoạt động giống như các đối tượng bình thường. Sự khác biệt là Vue có thể can thiệp vào việc truy cập và biến đổi của tất cả các thuộc tính của một đối tượng phản ứng để theo dõi và kích hoạt phản ứng.
reactive() chuyển đổi đối tượng một cách sâu: các đối tượng lồng nhau cũng được bọc bởi reactive() khi truy cập. Nó cũng được gọi bằng ref() bên trong khi giá trị ref là một đối tượng. Tương tự như refs nông, cũng có shallowReactive() API để tùy chọn không sử dụng phản ứng sâu.

4.1 Proxy Phản ứng so với Gốc **

Quan trọng là lưu ý rằng giá trị trả về từ reactive() là một Proxy của đối tượng gốc, không bằng với đối tượng gốc:

const raw = {}
const proxy = reactive(raw)

// proxy is NOT equal to the original.
console.log(proxy === raw) // false

Chỉ có proxy là phản ứng – biến đổi đối tượng gốc sẽ không kích hoạt cập nhật. Do đó, thực hành tốt nhất khi làm việc với hệ thống phản ứng của Vue là chỉ sử dụng các phiên bản proxy của trạng thái của bạn.
Để đảm bảo việc truy cập nhất quán vào proxy, gọi reactive() trên cùng một đối tượng luôn trả về cùng một proxy, và gọi reactive() trên một proxy hiện có cũng trả về cùng một proxy đó:

// calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true

// calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true

Quy tắc này cũng áp dụng cho các đối tượng lồng nhau. Do phản ứng sâu, các đối tượng lồng trong một đối tượng phản ứng cũng là proxy:

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

4.2 Hạn chế của reactive() **

API reactive() có một số hạn chế:
1. Loại giá trị hạn chế: nó chỉ hoạt động cho các loại đối tượng (đối tượng, mảng, và các loại bộ sưu tập như MapSet). Nó không thể giữ các loại nguyên thủy như string, number hoặc boolean.
2. Không thể thay thế toàn bộ đối tượng: vì theo dõi phản ứng của Vue hoạt động qua việc truy cập thuộc tính, chúng ta phải luôn giữ cùng một tham chiếu đến đối tượng phản ứng. Điều này có nghĩa là chúng ta không thể dễ dàng “thay thế” một đối tượng phản ứng vì kết nối phản ứng với tham chiếu đầu tiên đã bị mất:

let state = reactive({ count: 0 })

   // the above reference ({ count: 0 }) is no longer being tracked
   // (reactivity connection is lost!)
   state = reactive({ count: 1 })

3. Không thân thiện với việc giải cấu trúc: khi chúng ta giải cấu trúc thuộc tính loại nguyên thủy của một đối tượng phản ứng thành biến cục bộ, hoặc khi chúng ta truyền thuộc tính đó vào một hàm, chúng ta sẽ mất kết nối phản ứng:

const state = reactive({ count: 0 })

   // count is disconnected from state.count when destructured.
   let { count } = state
   // does not affect original state
   count++

   // the function receives a plain number and
   // won't be able to track changes to state.count
   // we have to pass the entire object in to retain reactivity
   callSomeFunction(state.count)

Do những hạn chế này, chúng tôi khuyên bạn nên sử dụng ref() như API chính để khai báo trạng thái phản ứng.

5. Chi tiết Bổ sung về Mở gói Ref **

5.1 Như Thuộc tính Đối tượng Phản ứng **

Một ref sẽ tự động được mở gói khi truy cập hoặc biến đổi như một thuộc tính của một đối tượng phản ứng. Nó hoạt động giống như một thuộc tính bình thường :

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

Nếu một ref mới được gán cho một thuộc tính liên kết với một ref hiện có, nó sẽ thay thế ref cũ:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// original ref is now disconnected from state.count
console.log(count.value) // 1

Việc mở gói ref chỉ xảy ra khi nằm trong một đối tượng phản ứng sâu. Nó không áp dụng khi được truy cập như một thuộc tính của một đối tượng phản ứng nông.

5.2 Lưu ý trong Mảng và Bộ sưu tập **

Không giống như các đối tượng phản ứng, không có quá trình mở gói được thực hiện khi ref được truy cập như một phần tử của một mảng phản ứng hoặc một loại bộ sưu tập nguyên thủy như Map:

const books = reactive([ref('Vue 3 Guide')])
// need .value here
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// need .value here
console.log(map.get('count').value)

5.3 Lưu ý khi Mở gói trong Template **

Việc mở gói ref trong các template chỉ áp dụng nếu ref là một thuộc tính cấp cao trong ngữ cảnh render của template.
Trong ví dụ dưới đây, countobject là các thuộc tính cấp cao, nhưng object.id thì không:

const count = ref(0)
const object = { id: ref(1) }

Do đó, biểu thức này hoạt động như mong đợi:

{{ count + 1 }}

…trong khi biểu thức này KHÔNG hoạt động:

{{ object.id + 1 }}

Kết quả hiển thị sẽ là [object Object]1object.id không được mở gói khi đánh giá biểu thức và vẫn là một đối tượng ref. Để sửa điều này, chúng ta có thể giải cấu trúc id thành một thuộc tính cấp cao:

const { id } = object
{{ id + 1 }}

Bây giờ kết quả hiển thị sẽ là 2.
Một điều cần lưu ý khác là một ref sẽ được mở gói nếu nó là giá trị được đánh giá cuối cùng của một nội suy văn bản (tức là một thẻ {{ }}), vì vậy đoạn mã sau sẽ hiển thị 1:

{{ object.id }}

Điều này chỉ là một tính năng tiện lợi của nội suy văn bản và tương đương với {{ object.id.value }}.

5.4 Phương thức có trạng thái *

Trong một số trường hợp, chúng ta có thể cần tạo động một hàm phương thức, ví dụ tạo một xử lý sự kiện trễ:

import { debounce } from 'lodash-es'

export default {
  methods: {
    // Debouncing with Lodash
    click: debounce(function () {
      // ... respond to click ...
    }, 500)
  }
}

Tuy nhiên, cách tiếp cận này gây vấn đề cho các thành phần được tái sử dụng vì một hàm trễ là có trạng thái: nó duy trì một số trạng thái nội bộ về thời gian đã trôi qua. Nếu nhiều phiên bản thành phần chia sẻ cùng một hàm trễ, chúng sẽ gây ảnh hưởng lẫn nhau.
Để giữ cho hàm trễ của mỗi phiên bản thành phần độc lập với nhau, chúng ta có thể tạo phiên bản trễ trong hook created vòng đời:

export default {
  created() {
    // each instance now has its own copy of debounced handler
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // also a good idea to cancel the timer
    // when the component is removed
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... respond to click ...
    }
  }
}

Trên Cafedev, chúng ta đã có cơ hội khám phá những cơ bản về tính phản ứng trong Vuejs. Hi vọng rằng thông qua bài viết này, bạn đã có cái nhìn rõ ràng hơn về cách Vuejs quản lý và phản ứng với dữ liệu trong ứng dụng web của mình. Hãy tiếp tục thực hành và khám phá thêm về sức mạnh của tính phản ứng trong Vuejs để xây dựng những ứng dụng web tuyệt vời hơn. Cảm ơn bạn đã đồng hành cùng 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!