前言 最近在跟朋友聊 Vue Composition API 的時候,順便翻了一下官方文件,剛好看到一個區塊滿有趣的就順便寫一篇文章來記錄哩。
Vue3 Composition API 變數宣告 基本上我們在 Vue3 Composition API 中宣告變數的方式有兩種,一種是使用 ref
,另一種是使用 reactive
,這兩種有什麼差別呢?底下是一個 Vue 範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template > <div > <p > {{ name }}</p > <button @click ="changeName" > 點我改名字</button > </div > </template > <script setup > import { ref } from 'vue' const name = ref ('我是誰!' )function changeName ( ) { name.value = 'Ray' } </script >
當你點擊「點我改名字」按鈕後,畫面上的文字會從「我是誰!」變成「Ray」。
而 reactive
的使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <div > <p > {{ name }}</p > <button @click ="changeName" > 點我改名字</button > </div > </template > <script setup > import { reactive } from 'vue' const state = reactive ({ name : '我是誰!' }) function changeName ( ) { state.name = 'Ray' } </script >
這兩種宣告方式就是 Vue3 Composition API 常見的變數宣告方式,我相信身為 Vue 開發者的你應該都不陌生。
Note 如果想更深入了解 ref
與 reactive
的差異,可以參考遲早你會面對的問題,Vue3 Composition API 的 ref 與 reactive 差異是什麼?
那麼為什麼突然提到變數宣告呢?因為某一天朋友在抱怨…
「前陣子我們在維護專案的時候,不小心有一個同事在 .value
補了 = null
,後來查才知道是同事的 VSCode 自動補全功能幫他補的,結果因為這行為導致了一個功能異常,真的是太可怕了!」
可能會有人認為這件事情可以透過 Code Review 來避免,但是這樣的事情真的是有可能發生,所以我就想到了 Vue 官方文件中有一個有趣的區塊,也就是「深入響應式系統」頁面底下有一個「與信號(Signal)的關係」…
信號(Signal) 其實前陣子 React 圈有在討論這個議題,但這邊我們並沒有打算深入探討 React 或是 Signal 這個議題,所以讓我們拉回到 Vue 的世界。
首先先讓我們來看一下範例:
See the Pen
Vue Signal by Ray (@hsiangfeng )
on CodePen .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template > <div > <p > {{ name() }}</p > <button @click ="changeName" > 點我改名字</button > </div > </template > <script setup > import { shallowRef, triggerRef } from 'vue' function createSignal (value, options ) { const r = shallowRef (value) const get = ( ) => r.value const set = (v ) => { r.value = typeof v === 'function' ? v (r.value ) : v if (options?.equals === false ) triggerRef (r) } return [get, set] } const [ name, setName ] = createSignal ('你是誰?' )const changeName = ( ) => { setName ('我是 Ray' ) } </script >
你會發現這邊跟我們以往使用 ref
或是 reactive
宣告變數的方式有點不一樣,我們是透過了一個自定義的 createSignal
函式來宣告變數,你會發現我們是透過 name()
來取得值,而不是 name
,如果要更新畫面的話,我們則是透過 setName
來更新值。
有沒有一種好像似曾相識的感覺呢?沒錯,這跟 React 的 useState
有點類似
1 2 3 import { useState } from 'react' const [ name, setName ] = useState ('你是誰?' )
但是這邊我們是透過 Vue3 Composition API 來實現的,所以你也可以這樣命名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template > <div > <p > {{ name() }}</p > <button @click ="changeName" > 點我改名字</button > </div > </template > <script setup > import { shallowRef, triggerRef } from 'vue' function useState (value, options ) { const r = shallowRef (value) const get = ( ) => r.value const set = (v ) => { r.value = typeof v === 'function' ? v (r.value ) : v if (options?.equals === false ) triggerRef (r) } return [get, set] } const [ name, setName ] = useState ('你是誰?' )const changeName = ( ) => { setName ('我是 Ray' ) } </script >
See the Pen
Vue Signal by Ray (@hsiangfeng )
on CodePen .
一整個超像 React 了吧(笑)
那麼我們接下來分析一下官方文件提供的 createSignal
函式吧!
createSignal 首先讓我們來看一下 createSignal
的函式:
1 2 3 4 5 6 7 8 9 10 11 import { shallowRef, triggerRef } from 'vue' function createSignal (value, options ) { const r = shallowRef (value) const get = ( ) => r.value const set = (v ) => { r.value = typeof v === 'function' ? v (r.value ) : v if (options?.equals === false ) triggerRef (r) } return [get, set] }
首先我們可以看到一開始引入了 shallowRef
跟 triggerRef
這兩個 API
shallowRef
:宣告建立一個 ref
,但這個 API 有一點特別,它並不會進行深層的監聽,也就是說當你更新了物件的其中一個屬性時,並不會觸發畫面更新
triggerRef
:則是用來強制觸發更新用,因為 shallowRef
並不會進行深層的監聽,所以當你更新了物件的其中一個屬性時,並不會觸發畫面更新,這時候你就可以透過 triggerRef
來強制觸發畫面更新
有點難懂這兩個 API 吧?讓我們直接來看一個範例,首先底下是一個使用 ref
的範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <div > <p > {{ state.name }}</p > <button @click ="changeName" > 點我改名字</button > </div > </template > <script setup > import { ref } from 'vue' const state = ref ({ name : '你是誰?' }) function changeName ( ) { state.value .name = '我是 Ray' } </script >
See the Pen
Vue Signal-2 by Ray (@hsiangfeng )
on CodePen .
這一段理論上是沒有什麼問題的,但如果今天變成改用 shallowRef
就不一樣了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <div > <p > {{ state.name }}</p > <button @click ="changeName" > 點我改名字</button > </div > </template > <script setup > import { shallowRef, triggerRef } from 'vue' const state = shallowRef ({ name : '你是誰?' }) function changeName ( ) { state.value .name = '我是 Ray' } </script >
See the Pen
Vue Signal-3 by Ray (@hsiangfeng )
on CodePen .
你會發現當你不管怎麼點擊「點我改名字」的按鈕,畫面永遠都不會更新,因為 shallowRef
並不會進行深層的監聽,所以當你更新了物件的其中一個屬性時,並不會觸發畫面更新,除非是替換整個物件:
1 2 3 4 5 6 const state = shallowRef ({ name : '你是誰?' }) state.value .name = '我是 Ray' state.value = { name : '我是 Ray' }
這時候你就可以透過 triggerRef
來強制觸發畫面更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template > <div > <p > {{ state.name }}</p > <button @click ="changeName" > 點我改名字</button > </div > </template > <script setup > import { shallowRef, triggerRef } from 'vue' const state = shallowRef ({ name : '你是誰?' }) function changeName ( ) { state.value .name = '我是 Ray' triggerRef (state) } </script >
See the Pen
Vue Signal-4 by Ray (@hsiangfeng )
on CodePen .
這時候可能會有人想說,那我直接用 ref
不就好了?為什麼要這麼麻煩使用 shallowRef
跟 triggerRef
呢?其主要原因是在比較大的專案上很容易遇到資料結構複雜的情況,這時候就會需要使用到效能更好的 shallowRef
搭配 triggerRef
來進行監聽與更新,畢竟 ref
會進行深層的監聽,這樣可能會有效能上的問題。
接下來我會針對 createSignal
函式每一行程式碼進行解釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { shallowRef, triggerRef } from 'vue' function createSignal (value, options ) { const r = shallowRef (value) const get = ( ) => r.value const set = (v ) => { r.value = typeof v === 'function' ? v (r.value ) : v if (options?.equals === false ) triggerRef (r) } return [get, set] }
其實整體來講官方給的範例是相當不錯的,也幫你將一些問題給避免掉了,例如如果建立變數時是一個物件的話,假設如下:
1 2 3 4 5 6 7 8 9 const [ name, setName ] = createSignal ({ name : '你是誰?' }) const changeName = ( ) => { setName ((v ) => { v.name = 'QQ 先生' }) }
你會發現是無法更新的,所以這時候你可以增加一個 equals 的屬性,來強制觸發更新,這樣就不用擔心物件的其中一個屬性更新時,畫面不會更新的問題哩~
1 2 3 4 5 6 7 8 9 const [ name, setName ] = createSignal ({ name : '你是誰?' }, { equals : false }) const changeName = ( ) => { setName ((v ) => { v.name = 'QQ 先生' }) }