由於 Vue 是一個資料驅動畫面更新的框架,
響應式系統是最基礎也是最重要的一環。
因此前面講解了幾個核心 API,
而這個篇章中就要歸納前面幾篇重點並講解響應式系統的核心觀念。
如果我要用一句話來解釋:
「所謂的響應式系統本質上就是 響應式資料+副作用函數 的結合」
剛開始學習前端的時候,看到這個副作用真的是一頭霧水
什麼外部狀態? 哪邊算外面?
什麼本地? 要這樣分牆內牆外喔?
還是來看程式碼吧,延續我們昨天 watch
的觀念。watch
本質上是監聽資料變化並執行副作用
想問大家第一次聽到副作用覺得是什麼,
我以為是什麼 debuff,寫程式就寫程式,沒事幫自己 debuff 幹嘛?
那我想我們需要先了解副作用的定義,
基本上你從 Google 搜尋 “什麼是電腦科學的副作用?” 後,
你會得到幾千幾百種說法,例如 :
講真的 ==
什麼是程式之外,什麼是附加功能,
some kind of ? the outside world ?
定義這麼抽象的情況下,我覺得一般人要理解這個概念根本難如登天,
那不如我們透過實際的例子來了解,就以昨天的 computed
跟 watch
來講。
const count = ref(1);
const doubleCount = computed(() => {
return count * 2;
});
//大家覺得這個 computed 有產生副作用嗎?
const x = ref(false);
watch(x, (newX) => {
launchRocket(newX);
});
x.value = true;
// 那大家覺得這個 watch 有產生副作用嗎
我們先不解答,來看看以下這兩個情境。
今天你正在一個房間裡面製作雪人,輸送帶左邊會送雪球進來,
然後你要把雪球加上手收跟眼睛鼻子變成雪人,
最後返回一個雪人給外面,之後就不甘你的事了。
那大家可以把這個房間,想成是作用域,
在這個作用域內沒有造成任何副作用,也可以稱為純函式。
你今天一樣坐在房間裡做雪人,但手賤去按了一個按鈕,你也不知道那是幹嘛的,
然後按下去就發射飛彈了。
但這也不是你的錯,因為他們不該在房間裡放這麼危險的東西,
這就是所謂的副作用,你除了返回值,還可能動到你這個房間外的任何東西,
或是你根本不做雪人,整天按按鈕。
經過上面的情境,我想應該很好的能了解何謂副作用,何為純函示,
所以一句話總結副作用就是:
「影響自身作用域 ( {}
) 外的所有操作皆可以被定義為副作用。」
我相信大家從上面的例子可以知道,
副作用之所以定義這麼抽象這麼難講,就是因為他的情境實在太多了,
改變 DOM 結構,開關暗黑模式,console.log
,修改全域變數,還有一大堆都可以算副作用。
還記得我們前面在 在 Vue 過氣前要學的第四件事 - 2025 了還要用 .value ? 中提到說
原生 JavaScript 並是沒有提供直接追蹤普通變數的變化這種功能的,
但是能夠監控物件屬性的讀取 get
和設定 set
,這就是我們後續會講解的核心機制。
利用這兩個操作,我們可以「攔截」對物件屬性的訪問與修改,
從而在資料變動時觸發額外行為,像是執行副作用。
而在 Vue3 中要實現這種監控,就是要靠 Proxy,它會在變化時通知系統,更新畫面或重新計算。
Proxy 是 ES6 引入的強大物件代理工具,
它能包裹一個物件,監控並攔截對該物件的各種操作(包括 get、set、deleteProperty 等),
讓我們得以在資料被讀取或修改時,做出對應的響應動作。
如果你前面有了解何謂副作用,那接下來我們就可以再更深入點,
我們今天把視角拉遠點,看著剛剛的房間,這是我們的副作用函數 Effect()
當我們今天讀取 get
一個 Effect()
,可以先把這個Effect()
收集起來,
到了 set
階段我們就把所有收集起來的 Effect()
都拿出來執行
經過上面的圖解,我想你應該能理解 set
& get
大概的概念。
那我們就開始看點程式碼吧。
下面這是段精簡過後的偽程式碼
let activeEffect
function track(target, key) {
if (activeEffect) {
//這邊就是把一個一個 `effect()`放到箱子中
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
function trigger(target, key) {
//這邊就是把 `effect()` 從箱子裝取出並執行
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
function reactive(obj) {
return new Proxy(obj, {
// 讀取階段把 effect() 放到箱子中
get(target, key) {
track(target, key)
return target[key]
},
// 設定階段把 effect() 從箱子中取出並執行
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
那如果你能理解上面這裡的話,我們就來看真真正正的原始碼吧。
這是 Vue 響應式系統背後用來實作 ref()
的 RefImpl
//node_modules>@vue>reactivity>dist>reactivity.global.js
class RefImpl {
constructor(value, isShallow2) {
this.dep = new Dep();
this["__v_isRef"] = true;
this["__v_isShallow"] = false;
this._rawValue = isShallow2 ? value : toRaw(value);
// 原始資料類型 ( string, number, boolean, etc. )
this._value = isShallow2 ? value : toReactive(value);
// 物件參考資料類型 ( Object, Array, etc. )
}
get value() {
// 略
return this._value;
}
set value(newValue) {
const oldValue = this._rawValue;
const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue);
newValue = useDirectValue ? newValue : toRaw(newValue);
if (shared.hasChanged(newValue, oldValue)) {
this._rawValue = newValue;
this._value = useDirectValue ? newValue : toReactive(newValue);
// 如果不是淺層或唯讀 那也轉給 reactive
{
this.dep.trigger({
// 略
}
}
}
}
那一樣在在 Vue 過氣前要學的第四件事 - 2025 了還要用 .value ? 中我們有提到
其實你全都用 ref 也無所謂
為甚麼我會這樣說,因為 ref
中其實是包含了 reactive
,
因為 ref
底層遇到物件類型的值還是會丟給 reactive
去做處理,
所以其實都用 ref
就好。
這邊要判斷兩次是因為一次是初始值,一次是響應式變化後的新值,
都要判斷是否需要轉給 reactive
處理。
今天的內容比較深入一點,為了要講解響應式系統背後的核心機制,
從前面的 ref
, reactive
, computed
,watch
搭配解析,
再來到偽程式碼,原始碼,一步一步深入到最底層,搭配圖片讓大家能理解概念。
那強調一下,底層做的處理遠不止這些,
包含我們包裝 effect()
箱子背後的數據結構是 WeakMap + Map、淺唯讀,深唯讀、調度機制,etc.
篇幅不足以容納這麼多資訊,因此我們就收在這裡。
那明天呢,我們會進入 Vue 3.6 的熱門話題 Alien-signals,
這將與我們的響應式機制息息相關,這也是為什麼我會選擇照這個順序介紹。
ref
也能處理物件類型的資料嗎