Appearance
響應式狀態
在 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),不需要使用 .valuevue
<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()
的底層實現是getter
、setter
,reactive()
的底層實現是proxy
。- Vue3 使用了
proxy
因此不支援 IE。