Skip to content
On this page

資料響應 reactive && ref

簡單說

若希望「畫面」與「資料」是連動的,應使用 Vue3 提供的 reactiveref 兩種響應資料方法之一。

reactive 聲明響應狀態

reactive() 可以將 物件陣列 資料進行響應處理,當資料更新畫面也會同步更新。原理是使用 Proxy 物件代理 來「代理」原本的目標資料,操作都是透過「代理」,同時「代理」也會對目標資料進行深入的監聽。

特色

基本操作

vue
<script setup>
import { reactive, nextTick } from 'vue'

const state = reactive({ count: 0 })

function plus() {
  state.count++

  nextTick(() => {
    // 訪問更新後的 dom
  })
}
</script>

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

DOM 更新時機

DOM 會因應資料響應而自動更新,但「不是同步」的,所以可以在 nextTick 函式訪問到更新後的 DOM ,以確保操作。

Proxy 代理與原始對象

reactive() 返回的是 proxy 物件代理 不是原始資料本身,只有修改「代理」是響應的,直接「更換」原始資料的對象,是不會響應的。

為了保證 reactive()「代理」資料的一致性,對同一個原始資料的對象做 reactive() 都會返回相同的「代理」,對已經存在「代理」的原始對象,再做一次 reactive 還是返回一樣的「代理」。

js
import { reactive, ref } from 'vue'

const data = { name: 'naiky' }

const reactiveData = reactive(data)
const reactiveReactiveData = reactive(reactiveData)
const reactiveDataAgin = reactive(data)

console.log(reactiveData === data) // false 代理不等於對象
console.log(reactiveData === reactiveReactiveData) // true 為保持資料一致性 代理再代理 還是返回 -> 「代理」
console.log(reactiveData === reactiveDataAgin) // true 為保持資料一致性,代理相同的對象都是同一個代理

直接更換原始對象 不會響應

js
import { reactive } from 'vue'

let data = { count: 0 }
const state = reactive(data)

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

❌ data = { count: 999 }
console.log(state) // { count: 0 }

👍 state.count = 999
console.log(state) // { count: 999 }

操作限制

reactive() 對於物件之外的型別都是不能使用的,對於響應資料「解構」、「重新賦值」都是會造成無法響應的原因,只有與「代理」互動才是響應的王道。

❌ 屬性賦值

js
import { reactive } from 'vue'

const data = { count: 0 }
const state = reactive(data)

let count = state.count // ❌ 重新賦變量
count++ // 資料不會響應

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

❌ 屬性解構

js
import { reactive, ref } from 'vue'

const data = { count: 0 }
const reactiveData = reactive(data)

let { count } = reactiveData // ❌ 屬性解構
count++ // data 資料不會被連動改變

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

❌ 代理重新賦值

js
import { reactive, ref } from 'vue'

let reactiveData = reactive({ count: 0 })
reactiveData = reactive({ count: 9 })

// { count: 0 } 將不再被追蹤

❌ 函式傳參

跟解構的情況是類似的,當屬性被解開帶入,「代理」就不再受到監聽,除非帶入整個「代理」。

js
import { reactive, ref } from 'vue'

let reactiveData = reactive({ count: 0 })

// 單純帶入資料到函式而已,也無法監聽 reactiveData.count 的變化
somefunction(reactiveData.count)

ref 定義響應變數

由於 reactive() 只能接受 物件 型別,所以 vue3 提供了 ref() 來讓所有型別都可以被接受。ref() 是以 物件屬性定義 object.defineproperty 作為底層原理。放入的資料都會寫入 value 屬性,操作時再以 .value 來存取資料。

提醒

物件 類別被放入 ref 中,會自動被轉換為 reactive()proxy 物件代理 方法。

特色

  • 使用對象:基本型別 string number boolean …所有型別
  • 在 vue3 顯示: RefImpl
  • 須透過 .value 取值

基本操作

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

const num = ref(0)

function plus() {
  num.value++ // 變更 num 的值
}
</script>

<template>
  <button @click="plus">+</button>
  <div>
    {{ num }}
  </div>
</template>

物件自動轉為代理

ref() 內容或 .value 賦值為 物件 類別時,會自動使用 reactive() 轉換為 Proxy 代理。

js
import { ref } from 'vue'

let state = ref(0)
console.log(state.value) // 0

state.value = { id: 1 }
console.log(state.value) // Proxy { id: 1 }

// ref 放入物件,會自動轉為 reactive() 處理
const data = ref({ id: 1 })
console.log(data.value) // Proxy { id: 1 }

模版自動解 .value

ref() 是在變數「頂層」時,模版中使用會自動解開 .value,不需要再另加 .value

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

const state = ref(0)
state.value++
</script>

<template>
  <div>
    {{ state + 1 }}
    <!-- 不需要 .value 值為 2 -->
  </div>
</template>

ref 不是變數「頂層」 模版無法自動解 value

如果不進行「操作」模版是可以直接顯示「結果」。但在模版無法操作,若要「解決」操作的問題:

  • 將它「解構」變為變數的「頂層」再輸出模版
  • 在模版操作中加上 .value
vue
<script setup>
import { reactive, ref } from 'vue'

const state = {
  data: ref(0),
}

// 解決方式一
const { data } = state
</script>

<template>
  <div>
    <!-- ❌ 顯示: [object Object]1 (無法解 value) ❌ -->
    {{ state.data + 1 }}

    <!-- 解決方式一 顯示: 1 -->
    {{ data + 1 }}

    <!-- 解決方式二 顯示: 1 -->
    {{ state.data.value + 1 }}

    <!-- 自動運算結果: 0 -->
    {{ state.data }}
  </div>
</template>

reactive 「物件」嵌入 ref 自動解 .value

ref 被崁在 reactive 「物件」內,它將會「自動解 .value 」,在操作中不需要加 .value ,當成一般屬性操作就可以了。

js
const state = reactive({
  count: ref(0),
})

state.count += 1

console.log(state.count) // 2

reactive 「陣列」嵌入 ref 還是要解 .value

ref 被崁入到 reactive 的「陣列」內,是不會自動被解 .value ,操作還是需要加上 .value

js
const state = reactive([ ref(0) ])
❌ state[0] += 1
👍 state[0].value += 1

總結

  • 只要 ref 在「頂層」操作就是要再加 .value
  • refreactive 「物件」內,操作 ref 不用加 .value
  • refreactive 「陣列」內,操作 ref 「要」加 .value

注意 log 打印響應式狀態

一般情境下,會認為 ① 的 log 會是更新之前的狀態,但實際上 ① log 打印的 target 也會是更新後的狀態! 這是基於 ref 是 proxy 代理,指的不是資料的本身,但針對更新前的操作還是成立的!

js
import { ref } from 'vue'

const data = ref([1, 2, 3])

resetData(data)

function resetData(stateData) {
  console.log(stateData.value) // ① 打印更新前的原始值 ?
  console.log(stateData.value.length) // ③ 3

  stateData.value.splice(0)
  console.log(stateData.value) // ② 打印更新后的值
}

實際會因為響應式,在 target 上有所變化

Reference