Vue3 變數的存取控制與效能最佳化

Mike
I am Mike
Published in
12 min readMar 8, 2024

一些 ref 進行存取的時候的規劃,還有官方案例的深度解析以及 vue 的響應式進階 API 使用,看完後可能會讓你有些想法。

在撰寫 Vue 要宣告具有響應式特性的資料的時候,我們會使用 ref 來定義資料,然後可以透過 .value 來去直接修改它。

// 宣告一個具有響應式的資料
const name = ref("");

// 更改變數的 value
name.value = "mike"

當我們今天遇到需要在子組件去修改父組件的狀態的時候,除了使用 emit 外,也可以利用像是 One-Way Data Flow 的方式,定義一個修改狀態的 function 然後用 props 傳遞下去。

One-Way Data Flow 其實就是 React 中的 Lifting State Up ( 狀態提升 ),概念是一樣的。

關於 Emit 與 One-Way Data Flow 的一些細節之前參加鐵人賽的時候我有針對這個主題去做一些介紹,可以搭配著觀看。
https://ithelp.ithome.com.tw/articles/10273655

<script setup>
const isOpen = ref(false);
const Toggle = () => isOpen.value = !isOpen.value;
</script>

<template>
<Header :Toggle="Toggle" />
<Nav :isOpen="isOpen" :Toggle="Toggle" />
</template>

到目前為止我相信大家應該都沒有什麼太大的問題,接下來我就要來探討一下變數存取控制這件事。

封裝 (Encapsulation)

在大型架構在開發的時候,我們很常會使用封裝的概念來撰寫我們的程式碼,舉個最簡單的例子來說,我們宣告了一個變數,但是我們要去修改這個變數不是利用直接 = 的方式來進行 set ,而是利用 function 來去做修改,隱藏中間的細節和過程,只透過 function (介面) 來操作變數。

// 宣告變數
let a = 1;

// 宣告 set function
const setCount = (int) => {
a = int;
}

// 使用 set function 修改變數
setCount(4);

在上面提到的例子中,透過函式 setCount來修改變數 count的值,而不是直接操作變數,就是一種封裝的實踐,這樣做可以控制變數 set 的過程,例如加入額外的檢查或邏輯。

然而這種做法大量的被現代前端框架的 React 運用的淋漓盡致,當今天使用 useState 的時候,就可以有 value 以及 set function 可以使用。

官方文件 : https://react.dev/reference/react/useState

import { useState } from 'react';

function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [address, setAddress] = useState('Taiwan');
}

我們可以不用去再另外定義 set function,身為一個 Vue 的開發者,我覺得 React 在 useState 這方面真的做的很不錯。在小型專案上面來說,我直接定義 ref 然後用 .value 去修改真的是超方便,但是當你今天需要 emit 或是 傳遞要傳遞 props 的時候你還是需要去定義 set function,這時候你的父層組件在修改 ref 可能會同時存在 .valueset function ,尤其你的專案或組件開始變大,相對來說會亂了一點。

舉個簡單的例子,我有一個 activeIdx狀態是需要去看網址的 query 參數來更改的,但是我同時會在一開始進來網頁的時候以及網址有變動的時候去改變 activeIdx狀態,從下面的例子可以看到說對於 set 一個狀態來說有太多的地方了,如果一接手別人的code看到這樣子會有一種彆扭的感覺。

const route = useRoute();

const activeIdx = ref(0);

const setActiveIdx = (idx) => {
activeIdx.value = idx;
};

watch(route, (newRoute) => {
activeIdx.value = newRoute.query.activeId; // 這邊去修改
});

onMounted(()=> {
setActiveIdx(route.query.activeId) // 這邊去修改
})

時間推移造成的問題!?

這很多時候是因為寫 code 的過程演進過來的,首先一開始可能並沒有定義 setActiveIdx 的必要,只需要透過 .value 去 set 就好,然後當需求越做越大的時候開始需要傳遞 props 你就會去定義 set function,再來又需要在父層一開始進來去設定 activeIdx狀態,隨時間增加一個接一個,然後時間又很趕,沒時間整理 code 就會出現這樣的情況。

統一你的狀態存取方式

現在在 Vue 的中大型開發上面,會有 composables (共用邏輯處理)以及 pinia (全域狀態管理)的使用,針對這些封裝的邏輯我們也是會有狀態以及提供 set function 來去操作,雖然有些東西並非強制一定要提供這些 set function,但是對於引入這些抽出去的狀態來說,利用封裝的概念提供 set function 是可以避免跟組件的狀態混淆以及增加 code 的可讀性,重要的是可以更好的追蹤程式的 flow。

所以我現在基本上在定義 ref 這類的響應式資料的時候,只要在規劃上面會拆組件或是稍微較複雜,我就會手動新增一個 set function,預先準備,來避免我前面提到的改變狀態的地方過多的問題,而且未來增加新的需求的時候你也可以直接透過 set function 來更改 value,方便的很。

const username = ref("");

const setUsername = (name) => username.value = name;

問題接著就出現了

如果我有很多個 ref 我是不是就需要很多的 set function…

const age = ref(28);

const userName = ref('Taylor');

const address = ref('Taiwan');

const setAge = (num) => age.value = num;

const setUserName = (name) => userName.value = name;

const setAddress = (addr) => address.value = addr;

這樣雖然解決了前面提到的問題,但是也出現了每次都要定義 set function 的麻煩, code 也變得有點多,所以有沒有一種方式是可以把 React 的 useState 在 Vue 裡面實現呢?

const [state, setState] = useState(initialState)

所以接下來為了要實現像是 React 的 useState 的語法,我們要寫一個 composables

先打個預防針,以下有部分的 code 可能會讓部分的 Vue使用者身體不適,請儘速離開此頁面。

我們可以先新增 composables/useState.js 的檔案,然後添加以下的code

import { shallowRef } from "vue";

export function useState(baseState) {
const state = shallowRef(baseState);
const update = (newValue) => {
state.value = newValue;
};
return [state, update];
}

這麼一來我們就可以在開發的時候使用跟 React 一樣的方式來定義資料。

<script setup>
import { useState } from "./composables/useState.js";
const [name, setName] = useState("mike");
const [info, setInfo] = useState({
name: "mike",
age: 12,
});
</script>

<template>
<h2>name: {{ name }}</h2>
<pre>info: {{ info }}</pre>

<input type="text" v-model="name" />

<button @click="setName('jacky')">set name</button>
<button @click="setInfo({ name: 'andy', age: 20 })">set info</button>
</template>

這樣的一個處理方式就可以減少每次在定義 ref 的時候都要再寫一個 set function ,造成 code 會很多很雜的問題,而且也不會失去響應式的特性!

這邊附上我的範例給大家參考!
https://codesandbox.io/p/devbox/vue3-usestate-w297jd

為什麼不是 ref ? 什麼是 shallowRef ?

shallowRef 是一個 Vue 的響應式進階 API,它是 ref 的淺作用層,它和 ref不同,shallowRef内部的值並不會轉換成響應式,只有針對 .value 的 set 才會是響應式,也就是第一層的部分。

shallowRef 常用於大型資料結構的效能最佳化與外部的狀態管理系統整合。

const state = shallowRef({ count: 1 })

// 不會觸發更新
state.value.count = 2

// 會觸發更新
state.value = { count: 2 }

當我們使用 useState 之後,就不能直接針對物件去 set value,需要將原本的物件加上新的值一起寫入

const [info, setInfo] = useState({
name: "mike",
age: 12,
});

// 把原本的物件加上新的值一起寫入
setInfo({
...info,
address: addr,
});

基本上就跟我直接替換整個 object 一樣,所以這邊就不需要使用 ref來增加無謂的效能消耗。

ref 的響應式是深度的,雖然這樣很直觀,但在資料量龐大時,深度響應性也會導致不小的效能負擔,因為每個屬性存取都會觸發 proxy 的追蹤。不過這種效能負擔通常只有在處理超大型資料或層級很深的物件時,例如一次渲染需 100,000+ 個屬性時,才會變得比較明顯。

所以 Vue 提供了一種解決方案,透過使用 shallowRefshallowReactive來繞過深度響應。shallow API 的狀態只在第一層是響應式的,對所有深度的物件不會做任何處理。這使得對深度屬性的存取變得更快,但是我們必須將所有深度物件的屬性視為不可變( immutable ),並且只能透過替換整個物件來觸發更新。

延伸閱讀

shallowRef
https://vuejs.org/api/reactivity-advanced.html

減少大型不可變資料的響應性開銷
https://vuejs.org/guide/best-practices/performance.html#reduce-reactivity-overhead-for-large-immutable-structures

這範例其實是官方提供的!?

這個範例其實是官方文件中的一個小demo,官方是使用 immer +composables 來完成這個像是 useState 的範例,但今天我的需求只是用 useState 來解決封裝的問題,所以這邊就不特別提到 immer,這邊附上官方的文件範例大家可以去看一下。

Integration with External State Systems

不要把重點放在看起來像 React !

許多的前端框架已經引入了與 Vue 中的 ref類似的響應式基礎類型,並稱之為「signal」。

Preact 和 Qwik 的 signal 設計與 Vue 的 shallowRef 非常相似,都透過 .value 來去 set 資料,而 Solid 和 Angular 就有提供像 set function 去 set 資料。

但我想表達的是重點不在語法的相似度上,而是要看這樣的語法設計概念上解決了我什麼問題,又不是說我今天寫了 useState 我就會失去 Vue 的響應式優點。

我用了 useState 我還是在寫 Vue,我還是享受 Vue 帶給我開發上的便利以及更好的效能優化,而且我也沒說一定要所有的 ref 都要改成這種做法,原本的 .value 就非常好用了,只是隨者專案越來越大的時後勢必就會利用一些程式設計上的概念搭配開發,以減少重複造輪子等問題,你看其他的前端框架也逐漸在使用類似的方法,你會說其他的框架也看起來像是 react 嗎? 只不過是以前的宗教戰爭留下的 PTSD 罷了。

最後

這次介紹了 Vue3 的 shallow API 以及分享了我自己在開發上面的一些看法,如果覺得不錯歡迎幫我分享給你的朋友。

還有我開設許多線上課程,其中有「前端工程師全家桶組合包」的前端一條龍組合包課程,其中還包含免費課程。

完整的 JS 到 Vue3 開發網站的規劃學習路線,帶你完整的學習打好基礎

課程網址:https://thecodingpro.com/bundles/f2e

如果你有興趣現在輸入折扣碼:mikemedium
(單堂課程折 200元 ,組合包再折 500 元)

有任何問題都可以加我的 line 來詢問我喔 line id : @mike_cheng

--

--

Mike
I am Mike

如果有一行code無法解決的bug,那就寫兩行!