iT邦幫忙

2022 iThome 鐵人賽

DAY 11
5
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 11

真的好想離開 Vue 3 新手村 - Day 11: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (下)

  • 分享至 

  • xImage
  •  

Outline

  • 上集回顧
  • reactive()
    • 特性與限制
    • 什麼情況會失去響應性
  • ref()
    • 特性與限制
    • 什麼情況會失去響應性
  • 兩個比一比

上集回顧

reactive()

  • 用 Proxy 來實作,主要是拿來處理物件型別資料的響應性。
  • 會回傳 Proxy 物件
  • 透過 Proxy 物件來操作原始物件,可以攔截對資料的操作

ref()

  • 用 getter 和 setter 來實作,主要是拿來處理基本型別資料的響應性,但是他也可以接受物件型別。
  • 會回傳 RefImpl 物件,底下有一個屬性 key'value'value 為資料
  • 傳入物件型別,會先用 reactive 取得 Proxy 物件後放到 value 屬性下

reactive()

特性與限制

  • 參數只接受物件型別(例如:Object、Array、Map、Set),建議保持追蹤資料的參照 (reference) 一致
  • 預設為 deeply reactive,若為巢狀物件,所有巢狀資料都會有響應性
  • ref 資料傳進去 reactive() 會自動被 unwrap(取出 value 值),除非 ref 資料為響應式陣列或 Map

什麼情況會失去響應性

  • 對 reactive 物件 (Proxy) 重新賦值,因為新物件不再是 Proxy,所以會失去響應性
let objReactive = reactive({ name: "obj" });
console.log(`重新賦值前 objReactive`, objReactive);
objReactive = { name: "new-obj" };
console.log(`重新賦值後 objReactive`, objReactive);

常見情境與解決方法:Object.assign()

什麼情境下會想對 reactive 物件重新賦值?
舉個例子,我們定義了一個 reactive 物件,用來儲存使用者的資料,從 API 拿到新的使用者資料後,想要整筆替換掉,但仍要維持響應性。
請看底下錯誤使用範例:

const user = reactive({})

async function getUsers() {
    try {
      const response = await axios.get(url, config)
      //使用者資料現在在 data 裡
      const { data } = response 
      //想要把 data 裝進 user 裡
      //用這個方法會失去響應性!
      user = data
    } catch (error) {
     console.log(error)
    }
}

這時候就可以用 Object.assign()!

Object.assign() 被用來複製一個或多個物件自身所有可數的屬性到另一個目標物件。回傳的值為該目標物件。MDN - Object.assign()

透過 Object.assign 可以將新物件上的屬性,全部透過 Proxy 物件,複製一份,寫入原物件下,避免了重新賦值,維持了響應性。
上面為情境示意,用底下來的程式碼來實驗看看:

const objReactive = reactive({ name: "obj" });
console.log(`原本的`, objReactive);
Object.assign(objReactive, { name: "new-obj" });
console.log(`透過 Object.assign 修改的`, objReactive);

但是

這個方法有一個缺陷,透過 Object.assign 這個方法,是根據新物件去新增、修改原物件(透過 Proxy),所以,他不會比對並刪除舊有、新無的屬性。
也就是說,如果原物件有 type 屬性,但新物件沒有,透過 Object.assign 更新資料後,這個 type 屬性還是會留在原物件上。

const objReactive = reactive({ name: "obj", type: "Proxy" });
console.log(`原本的`, objReactive);
Object.assign(objReactive, { name: "new-obj" });
console.log(`透過 Object.assign 修改的`, objReactive);


屬性值只要脫離 Proxy,就失去響應性

  • 透過解構或賦值,將 reactive object 屬性值存入區域變數,區域變數只拿到純值或參照,不能響應 reactive object 屬性的改變。
const objReactive = reactive({ name: "obj" });

//會失去響應性,name1 拿到的是單純的字串
let { name: name1 } = objReactive;
console.log(`name1: ${name1}, ${typeof name1}`);

//會失去響應性,name2 拿到的是單純的字串
let name2 = objReactive.name;
console.log(`name2: ${name2}, ${typeof name2}`);

ref

特性與限制

  • 參數可以傳入任何型別
  • 會回傳具有響應式、可變動的 ref 物件(RefImpl),參數內容裝在該物件的value 屬性下
  • 即使傳入的資料為物件,依然可以對RefImpl.value做重新賦值,因為每次要對 .value 重新賦值都會觸發 RefImpl 的 setter 處理響應性。
const objectRef = ref({ count: 0 });
console.log(`前`, objectRef);

objectRef.value = { count: 1 };
console.log(`後`, objectRef);

*圖片說明:依然是那個 RefImpl,不過底下 value 屬性對應的值(物件參照)已經改變了。

  • 在 template 中使用 ref 資料

    • top-level binding 會自動 unwrap(取出 value 值),所以不用加 .value
    • 物件底下的 ref 值不是 top-level binding,依然可以成功讀取,但 template 中使用的值必須是最終計算所得的值,也就是不能用非 top-level binding 在模板中做運算。
    const object = { foo: ref(1) }
    
    //放到 template 中
    {{ obj.foo }} //不用運算可以正常顯示「1」
    
    • 要對物件底下的 ref 做運算,需要把 ref 變成 top-level binding
    const object = { foo: ref(1) }
    
    //把 foo 變數變成 top-level binding
    const { foo } = object
    
    //放到 template 中
    //要對物件底下的 ref 做運算,需要把 ref 變成 top-level binding
    {{ object.foo + 1 }} //會顯示「[Object Object] 1」
    {{ foo + 1 }}        //會顯示「2」
    

註:top-level binding 指的是這個 scope 內宣告在最外層的變數。

什麼情況會失去響應性

ref() 透過創造回傳的 reference (RefImpl),來維持響應性,而不是資料本身,所以基本型別才能透過 ref() 來達成響應。

  • valueref 物件中解構出來存到變數中,拿到的是當下的純值,這個變數沒有響應性。
const numberRef = ref(11);
const { value } = numberRef;
numberRef.value = 101;

//value 變數沒有響應性
console.log(value); //印出 11

當傳入的資料為物件,ref() 會用 reactive() 取得對應的 Proxy 物件,這時候對 objectRef.value 的做操作限制就跟上面的 reactive() 提到的限制相同。

比較

理解兩者的達成響應方式的差異後,兩個不同的用法理論上會很好記~

reactive ref
接收參數 物件型別 都可以
回傳 Proxy 物件 RefImpl 物件
取值 同普通物件 .value

兩者最大的差異是可以接收的資料型態,基礎型別一定要使用 ref

  • reactive:物件型別
  • ref:所有型別

還有,在處理物件型別資料的響應時:

  • reactive() 重新賦值就變成一般物件了,(可以考慮改用 Object.assign 處理,但是要小心原有屬性不會被刪除,而是根據新物件去新增、修改)
  • RefImpl.value 重新賦值時,setter 內會透過 reactive 將新物件轉為 Proxy,依然可以透過 RefImpl 這個參照讀寫 value 內的新物件,但要注意資料的參照已經不一樣了

所以網路上有流傳一種說法是,ref 用到底就對了!反而比較不會出錯!

真的假的??

還真的是這樣!
聽起來有點聳動,繼續看進一步說明:

就我目前的使用經驗歸納下來~
沒有 ref 做不到但 reactive 做得到的情境
只有reactive 做不到,但 ref 做得到的情境,目前想到的情境有二:

  1. 基本型別的響應
  2. v-model 的多選綁定

要說 ref 的缺點...
就是在 <script> 中每次取 ref 物件的值,都需要 .value 才能拿到,資料一多起來,說實話是有點阿雜 ಠ_ಠ

總之,還是建議新手多嘗試踩坑,才會知道什麼情境下適合使用哪一個 API!

註:上面這段是由開發者角度去看,對 Vue 3 框架來說,reactive() 當然是必須的,實際上,所有非基本型別的響應處理,Vue 3 都是透過 reactive() 來處理。

參考資料


上一篇
真的好想離開 Vue 3 新手村 - Day 10: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (上)
下一篇
真的好想離開 Vue 3 新手村 - Day 12: 認識 nextTick 與 DOM 響應更新時機 feat. template ref
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
wendy
iT邦新手 2 級 ‧ 2022-09-27 22:00:26

真的是一個坑xd/images/emoticon/emoticon13.gif

安揪拉 iT邦新手 4 級 ‧ 2022-09-28 12:17:36 檢舉

真的坑QQ /images/emoticon/emoticon02.gif

0
wendy
iT邦新手 2 級 ‧ 2022-09-27 22:00:27

真的是一個坑xd/images/emoticon/emoticon13.gif

0
deron
iT邦新手 5 級 ‧ 2022-09-29 09:21:16

能用統一的reactiveApi還是用統一的/images/emoticon/emoticon02.gif
ref在有些情況,如template中會自動解構出.value,其他情況又要手動,容易造成代碼更難讀。

安揪拉 iT邦新手 4 級 ‧ 2022-11-09 20:28:53 檢舉

如果非基本型別&不需要重新賦值,用 reactive 的確更乾淨俐落~!

0
Amigo
iT邦新手 5 級 ‧ 2023-11-23 15:25:36

解決了

我要留言

立即登入留言