Skip to content

響應式狀態

在 MVVM 架構下,Model 需要具有響應性,才能讓 ViewModel 自動更新視圖。響應性是指 Model 的狀態變化會自動通知 ViewModel,ViewModel 才能根據 Model 的最新狀態更新視圖。

響應性狀態有3特性:

  • 管理一個私有值(例如 _value),並且存取和修改該值都只能透過這個響應性狀態。
  • 管理一個對象清單,紀錄私有值改變時通知應通知的對象。
  • 在私有值被修改/賦值時通知依賴於私有值的對象。

ref()

  • ref 依賴跟蹤原理基於 getter、setter,所以取用時最後會加上一個 value,這是官方給的示意程式碼 (非實作):
    js
    // pseudo code, not actual implementation
    const myRef = {
      _value: 0,
      get value() {
        track() // track: 加入觀察者
        return this._value
      },
      set value(newValue) {
        this._value = newValue
        trigger() // trigger: 通知觀察者
      }
    }
    // pseudo code, not actual implementation
    const myRef = {
      _value: 0,
      get value() {
        track() // track: 加入觀察者
        return this._value
      },
      set value(newValue) {
        this._value = newValue
        trigger() // trigger: 通知觀察者
      }
    }
  • template 裡面取用 ref 會自動解包(unwrap),不需要使用 .value
    vue
    <template>
      <!-- template 裡面取用 ref 會自動解包(unwrap),不需要使用 .value -->
      <div>{{ count }}</div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    const count = ref(0)
    
    console.log(count) // { value: 0 }
    console.log(count.value) // 0
    
    count.value++
    console.log(count.value) // 1
    </script>
    <template>
      <!-- template 裡面取用 ref 會自動解包(unwrap),不需要使用 .value -->
      <div>{{ count }}</div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    const count = ref(0)
    
    console.log(count) // { value: 0 }
    console.log(count.value) // 0
    
    count.value++
    console.log(count.value) // 1
    </script>

試試看

以觀察者模式理解

我們可以透過不到短短50行的程式碼實作模擬簡單的 MVVM(Model-View-ViewModel)架構 ref,幫助我們理解:

js
// vue 透過類似機制宣告為響應式狀態
class Ref {
  constructor(value) {
    Ref.target = null;
    this.dep = []; // 存儲所有觀察者的清單

    let _value = value;
    // 新增觀察者
    const track = () => Ref.target && this.dep.push(Ref.target); 
    // 通知所有觀察者更新
    const trigger = () => this.dep.forEach(dep => dep.update()); 
    // 回傳響應式狀態
    return Object.defineProperty({}, 'value', {
      get() {
        track();
        return _value;
      },
      set(newValue) {
        _value = newValue;
        trigger();
      },
    });
  }
}

const ref = value => new Ref(value);

// 觀察者,模板綁定響應式資料透過類似的機制獲得更新
class Watcher {
  constructor(template, item = {}) {
    this.template = template;
    this.ref = item;
    Object.keys(item).forEach(ref => {
      Ref.target = this;
      const _ = item[ref].value; // 將自己添加到觀察者清單
      Ref.target = null;
    });
  }
  update() {
    // 替換模板中的變數
    const el = this.template.replace(
      /\{\{\s*([a-zA-Z]+)\s*\}\}/g, 
      (match, key) => this.ref[key]?.value || match
    );
    console.log(el);
  }
}
// vue 透過類似機制宣告為響應式狀態
class Ref {
  constructor(value) {
    Ref.target = null;
    this.dep = []; // 存儲所有觀察者的清單

    let _value = value;
    // 新增觀察者
    const track = () => Ref.target && this.dep.push(Ref.target); 
    // 通知所有觀察者更新
    const trigger = () => this.dep.forEach(dep => dep.update()); 
    // 回傳響應式狀態
    return Object.defineProperty({}, 'value', {
      get() {
        track();
        return _value;
      },
      set(newValue) {
        _value = newValue;
        trigger();
      },
    });
  }
}

const ref = value => new Ref(value);

// 觀察者,模板綁定響應式資料透過類似的機制獲得更新
class Watcher {
  constructor(template, item = {}) {
    this.template = template;
    this.ref = item;
    Object.keys(item).forEach(ref => {
      Ref.target = this;
      const _ = item[ref].value; // 將自己添加到觀察者清單
      Ref.target = null;
    });
  }
  update() {
    // 替換模板中的變數
    const el = this.template.replace(
      /\{\{\s*([a-zA-Z]+)\s*\}\}/g, 
      (match, key) => this.ref[key]?.value || match
    );
    console.log(el);
  }
}

測試看看:

js
// vue 透過類似機制宣告為響應式狀態
var count = ref(0);
var message = ref('message');
// 模板綁定響應式資料透過類似的機制獲得更新
var template = '<div>{{count}} ({{message}})</div>';
new Watcher(template, { count, message });
// vue 透過類似機制宣告為響應式狀態
var count = ref(0);
var message = ref('message');
// 模板綁定響應式資料透過類似的機制獲得更新
var template = '<div>{{count}} ({{message}})</div>';
new Watcher(template, { count, message });

依序測試看看:

js
count.value++;
message.value="new message";
count.value++;
message.value="new message2";
a = (count.value++) && (message.value="new message ABC")
count.value++;
message.value="new message";
count.value++;
message.value="new message2";
a = (count.value++) && (message.value="new message ABC")

元件參考

別忘了元件本質上也是 javascript 物件,當然也可以使用 ref 進行跟蹤取值、調用暴露的方法:

vue
<template>
  <dialog ref="dialog"></dialog>
  <button @click="test"> click </button>
</template>
<script setup>
import { ref } from 'vue';
const dialog = ref(null)
const test = ()=>{
  dialog.value.show()
};
</script>
<template>
  <dialog ref="dialog"></dialog>
  <button @click="test"> click </button>
</template>
<script setup>
import { ref } from 'vue';
const dialog = ref(null)
const test = ()=>{
  dialog.value.show()
};
</script>

shallowRef()

Deep Reactivity:ref 預設會跟蹤其內部的所有對象,如果不是基本(primative)數據類型,它會自動使用 reactive() 來將對象轉換為代理(proxy)。 淺層(Shallow) refs:用於改進性能(當您不希望跟蹤 ref 下的大型對象時),或是 ref 內部對象的狀態由外部函數庫管理。

js
const state = shallowRef({ count: 1 })
state.value.count = 2 // 無反應
state.value = { count: 2 } // 有反應
const state = shallowRef({ count: 1 })
state.value.count = 2 // 無反應
state.value = { count: 2 } // 有反應

reactive()

  • 原理是 javascript proxy。
  • 只能包裝物件(object, Array, Map/Set ...),不能包裝基本數據類型。
  • 使用時不需要加 .value。
vue
<template>
  <button @click="state.count++">
    {{ state.count }}
  </button>
</template>

<script setup>
  import { reactive } from 'vue';
  const state = reactive({ count: 0 });
  console.log(state.count) // 0
</script>
<template>
  <button @click="state.count++">
    {{ state.count }}
  </button>
</template>

<script setup>
  import { reactive } from 'vue';
  const state = reactive({ count: 0 });
  console.log(state.count) // 0
</script>

試試看

shallowReactive()

同 shallwRef()

常犯的錯誤

  • 直接把變數指向其他響應式數據(以為更改響應式數據內容)
js
let state = reactive({ count: 0 })
// state 現在指向 reactive({ count: 1 }) 這個 proxy
state = reactive({ count: 1 })
// 現在沒有變數指向 reactive({ count: 0 }) 這個 proxy
let state = reactive({ count: 0 })
// state 現在指向 reactive({ count: 1 }) 這個 proxy
state = reactive({ count: 1 })
// 現在沒有變數指向 reactive({ count: 0 }) 這個 proxy
  • 解構賦值(destructure)的內容失去響應性
js
const state = reactive({ count: 0 })
// count 現在就只是一個初級資料結構,沒有響應性
let { count } = state
// 不會影響到 state.count
count++
const state = reactive({ count: 0 })
// count 現在就只是一個初級資料結構,沒有響應性
let { count } = state
// 不會影響到 state.count
count++

試看看

nextTick()

Vue 會收集 reactive state 的變更,然後一次性重新渲染 DOM,因此,當 reactive state 改變後,如果需要從 DOM 中獲取已更改的值,必須使用 nextTick()nextTick() 返回一個 Promise,因此可以使用 await 進行等待。

試試看

Summary

  • 響應性狀態3特性:
    • 管理一個私有值,並且存取和修改該值都只能透過這個響應性狀態。
    • 管理一個對象清單,紀錄私有值改變時通知應通知的對象。
    • 在私有值被修改/賦值時通知依賴於私有值的對象。
  • ref()reactive() 可包裹私有值,獲得響應性狀態。
  • ref() 用於包裹初級(primary)資料型態,也可以包裹物件但會自動調用 reactive()
  • reactive 用於包裹複雜資料型態。
  • ref() 可以製作元件參考
  • ref()的底層實現是 gettersetterreactive() 的底層實現是 proxy
  • Vue3 使用了 proxy 因此不支援 IE。

Reference