iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Vue.js

在 Vue 過氣前要學的三十件事系列 第 6

在 Vue 過氣前要學的第六件事 - 響應式到底為什麼那麼重要

  • 分享至 

  • xImage
  •  

前言

由於 Vue 是一個資料驅動畫面更新的框架,
響應式系統是最基礎也是最重要的一環。

因此前面講解了幾個核心 API,
而這個篇章中就要歸納前面幾篇重點並講解響應式系統的核心觀念。

如果我要用一句話來解釋:
「所謂的響應式系統本質上就是 響應式資料+副作用函數 的結合」

副作用函數 ? 純函式?

剛開始學習前端的時候,看到這個副作用真的是一頭霧水
什麼外部狀態? 哪邊算外面?
什麼本地? 要這樣分牆內牆外喔?
https://ithelp.ithome.com.tw/upload/images/20250905/20172784tVYDHI8ZMf.png

還是來看程式碼吧,延續我們昨天 watch 的觀念。
watch 本質上是監聽資料變化並執行副作用

想問大家第一次聽到副作用覺得是什麼,
我以為是什麼 debuff,寫程式就寫程式,沒事幫自己 debuff 幹嘛?
https://ithelp.ithome.com.tw/upload/images/20250905/20172784ggpaVof0zX.png

那我想我們需要先了解副作用的定義,
基本上你從 Google 搜尋 “什麼是電腦科學的副作用?” 後,

你會得到幾千幾百種說法,例如 :
https://ithelp.ithome.com.tw/upload/images/20250905/20172784PPoZtYLcis.png
講真的 ==
https://ithelp.ithome.com.tw/upload/images/20250905/20172784YIrYneEJ43.jpg

什麼是程式之外,什麼是附加功能,
some kind of ? the outside world ?

定義這麼抽象的情況下,我覺得一般人要理解這個概念根本難如登天,
那不如我們透過實際的例子來了解,就以昨天的 computedwatch 來講。

const count = ref(1);
const doubleCount = computed(() => {
  return count * 2;
});
//大家覺得這個 computed 有產生副作用嗎?
const x = ref(false);

watch(x, (newX) => {
  launchRocket(newX);
});

x.value = true;
// 那大家覺得這個 watch 有產生副作用嗎

我們先不解答,來看看以下這兩個情境。

情境一

今天你正在一個房間裡面製作雪人,輸送帶左邊會送雪球進來,
然後你要把雪球加上手收跟眼睛鼻子變成雪人,
最後返回一個雪人給外面,之後就不甘你的事了。

那大家可以把這個房間,想成是作用域
在這個作用域內沒有造成任何副作用,也可以稱為純函式。

https://ithelp.ithome.com.tw/upload/images/20250905/20172784BbcHNZIWMm.png

那再來一個情境

你今天一樣坐在房間裡做雪人,但手賤去按了一個按鈕,你也不知道那是幹嘛的,
然後按下去就發射飛彈了。

但這也不是你的錯,因為他們不該在房間裡放這麼危險的東西,
這就是所謂的副作用,你除了返回值,還可能動到你這個房間外的任何東西

或是你根本不做雪人,整天按按鈕。
https://ithelp.ithome.com.tw/upload/images/20250905/20172784AYoBpnlCHP.png

經過上面的情境,我想應該很好的能了解何謂副作用,何為純函示,
所以一句話總結副作用就是:

「影響自身作用域 ( {} ) 外的所有操作皆可以被定義為副作用。」

我相信大家從上面的例子可以知道,
副作用之所以定義這麼抽象這麼難講,就是因為他的情境實在太多了,
改變 DOM 結構,開關暗黑模式,console.log,修改全域變數,還有一大堆都可以算副作用。

響應式資料是什麼?

還記得我們前面在 在 Vue 過氣前要學的第四件事 - 2025 了還要用 .value ? 中提到說
原生 JavaScript 並是沒有提供直接追蹤普通變數的變化這種功能的,

但是能夠監控物件屬性的讀取 get 和設定 set,這就是我們後續會講解的核心機制。

利用這兩個操作,我們可以「攔截」對物件屬性的訪問與修改,
從而在資料變動時觸發額外行為,像是執行副作用。

而在 Vue3 中要實現這種監控,就是要靠 Proxy,它會在變化時通知系統,更新畫面或重新計算。

Proxy 是 ES6 引入的強大物件代理工具,
它能包裹一個物件,監控並攔截對該物件的各種操作(包括 get、set、deleteProperty 等),
讓我們得以在資料被讀取或修改時,做出對應的響應動作。

如果你前面有了解何謂副作用,那接下來我們就可以再更深入點,
我們今天把視角拉遠點,看著剛剛的房間,這是我們的副作用函數 Effect()
https://ithelp.ithome.com.tw/upload/images/20250905/20172784aKJ77QOWzQ.png
當我們今天讀取 get 一個 Effect(),可以先把這個Effect()收集起來,
https://ithelp.ithome.com.tw/upload/images/20250905/20172784PAMp1RevoI.png
到了 set 階段我們就把所有收集起來的 Effect() 都拿出來執行
https://ithelp.ithome.com.tw/upload/images/20250905/20172784A0LNXw5TZf.png

我想了解更深一點,能不能簡單講一下底層差異?


經過上面的圖解,我想你應該能理解 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,
這將與我們的響應式機制息息相關,這也是為什麼我會選擇照這個順序介紹。

一些小練習

  1. 你可以一句話解釋何謂副作用給沒聽過的人嗎?
  2. Vue 的響應式核心其實是哪兩者的搭配?
  3. 所以你知道為什麼 ref 也能處理物件類型的資料嗎

資料來源

  1. Vue Doc #深入響應式系統
  2. Vue.js 設計實戰

上一篇
在 Vue 過氣前要學的第五件事 - 主動還是被動
系列文
在 Vue 過氣前要學的三十件事6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言