記錄--源碼視角,Vue3為什麼推薦使用ref而不是reactive

来源:https://www.cnblogs.com/smileZAZ/p/18023786
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 ref 和 reactive 是 Vue3 中實現響應式數據的核心 API。ref 用於包裝基本數據類型,而 reactive 用於處理對象和數組。儘管 reactive 似乎更適合處理對象,但 Vue3 官方文檔更推薦使用 ref。 我 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

ref 和 reactive 是 Vue3 中實現響應式數據的核心 API。ref 用於包裝基本數據類型,而 reactive 用於處理對象和數組。儘管 reactive 似乎更適合處理對象,但 Vue3 官方文檔更推薦使用 ref

 我的想法,ref就是比reactive好用,官方也是這麼說的,不服來踩!下麵我們從源碼的角度詳細討論這兩個 API,以及 Vue3 為什麼推薦使用ref而不是reactive

ref 的內部工作原理

ref 是一個函數,它接受一個內部值並返回一個響應式且可變的引用對象。這個引用對象有一個 .value 屬性,該屬性指向內部值。

// 深響應式
export function ref(value?: unknown) {
  return createRef(value, false)
}

// 淺響應式
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

function createRef(rawValue: unknown, shallow: boolean) {
  // 如果傳入的值已經是一個 ref,則直接返回它
  if (isRef(rawValue)) {
    return rawValue
  }
  // 否則,創建一個新的 RefImpl 實例
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  // 存儲響應式的值。我們追蹤和更新的就是_value。(這個是重點)
  private _value: T
  // 用於存儲原始值,即未經任何響應式處理的值。(用於對比的,這塊的內容可以不看)
  private _rawValue: T 

  // 用於依賴跟蹤的 Dep 類實例
  public dep?: Dep = undefined
  // 一個標記,表示這是一個 ref 實例
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    // 如果是淺響應式,直接使用原始值,否則轉換為非響應式原始值
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 如果是淺響應式,直接使用原始值,否則轉換為響應式值
    this._value = __v_isShallow ? value : toReactive(value)
    
    // toRaw 用於將響應式引用轉換回原始值
    // toReactive 函數用於將傳入的值轉換為響應式對象。對於基本數據類型,toReactive 直接返回原始值。
    // 對於對象和數組,toReactive 內部會調用 reactive 來創建一個響應式代理。
    // 因此,對於 ref 來說,基本數據類型的值會被 RefImpl 直接包裝,而對象和數組
    // 會被 reactive 轉換為響應式代理,最後也會被 RefImpl 包裝。
    // 這樣,無論是哪種類型的數據,ref 都可以提供響應式的 value 屬性,
    // 使得數據變化可以被 Vue 正確追蹤和更新。
    // export const toReactive = (value) => isObject(value) ? reactive(value) : value
  }

  get value() {
    // 追蹤依賴,這樣當 ref 的值發生變化時,依賴這個 ref 的組件或副作用函數可以重新運行。
    trackRefValue(this)
    // 返回存儲的響應式值
    return this._value
  }

  set value(newVal) {
    // 判斷是否應該使用新值的直接形式(淺響應式或只讀)
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    // 如果需要,將新值轉換為非響應式原始值
    newVal = useDirectValue ? newVal : toRaw(newVal)
    // 如果新值與舊值不同,更新 _rawValue 和 _value
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      // 觸發依賴更新
      triggerRefValue(this, DirtyLevels.Dirty, newVal)
    }
  }
}

在上述代碼中,ref 函數通過 new RefImpl(value) 創建了一個新的 RefImpl 實例。這個實例包含 getter 和 setter,分別用於追蹤依賴和觸發更新。使用 ref 可以聲明任何數據類型的響應式狀態,包括對象和數組。

import { ref } from 'vue' 

const state = ref({ count: 0 })
state.value.count++

註意,ref核心是返回響應式且可變的引用對象,而reactive核心是返回的是響應式代理,這是兩者本質上的核心區別,也就導致了ref優於reactive,我們接著看下reactive源碼實現。

reactive 的內部工作原理

reactive 是一個函數,它接受一個對象並返回該對象的響應式代理,也就是 Proxy

function reactive(target) {
  if (target && target.__v_isReactive) {
    return target
  }

  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

function createReactiveObject(
  target,
  isReadonly,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  if (!isObject(target)) {
    return target
  }
  
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  
  const proxy = new Proxy(target, baseHandlers)
  proxyMap.set(target, proxy)
  return proxy
}
reactive的源碼相對就簡單多了,reactive 通過 new Proxy(target, baseHandlers) 創建了一個代理。這個代理會攔截對目標對象的操作,從而實現響應式。
import { reactive } from 'vue' 

const state = reactive({ count: 0 })
state.count++

到這裡我們可以看出 refreactive 在聲明數據的響應式狀態上,底層原理是不一樣的。ref 採用 RefImpl對象實例,reactive採用Proxy代理對象。

ref 更深入的理解

當你使用 new RefImpl(value) 創建一個 RefImpl 實例時,這個實例大致上會包含以下幾部分:

  1. 內部值:實例存儲了傳遞給構造函數的初始值。
  2. 依賴收集:實例需要跟蹤所有依賴於它的效果(effect),例如計算屬性或者副作用函數。這通常通過一個依賴列表或者集合來實現。
  3. 觸發更新:當實例的值發生變化時,它需要通知所有依賴於它的效果,以便它們可以重新計算或執行。

RefImpl 類似於發佈-訂閱模式的設計,以下是一個簡化的 RefImpl 類的偽代碼實現,展示這個實現過程:

class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  notify() {
    this.subscribers.forEach(effect => effect());
  }
}

let activeEffect = null;

function watchEffect(effect) {
  activeEffect = effect;
  effect();
  activeEffect = null;
}

class RefImpl {
  constructor(value) {
    this._value = value;
    this.dep = new Dep();
  }

  get value() {
    // 當獲取值時,進行依賴收集
    this.dep.depend();
    return this._value;
  }

  set value(newValue) {
    if (newValue !== this._value) {
      this._value = newValue;
      // 值改變時,觸發更新
      this.dep.notify();
    }
  }
}

// 使用示例
const count = new RefImpl(0);

watchEffect(() => {
  console.log(`The count is: ${count.value}`); // 訂閱變化
});

count.value++; // 修改值,觸發通知,重新執行watchEffect中的函數
Dep 類負責管理一個依賴列表,並提供依賴收集和通知更新的功能。RefImpl 類包含一個內部值 _value 和一個 Dep 實例。當 value 被訪問時,通過 get 方法進行依賴收集;當 value 被賦予新值時,通過 set 方法觸發更新。

refreactive 儘管兩者在內部實現上有所不同,但它們都能滿足我們對於聲明響應式變數的要求,但是 reactive 卻存在一定的局限性。

reactive 的局限性

在 Vue3 中,reactive API 通過 Proxy 實現了一種響應式數據的方法,儘管這種方法在性能上比 Vue2 有所提升,但 Proxy 的局限性也導致了 reactive 的局限性,這些局限性可能會影響開發者的使用體驗。

僅對引用數據類型有效

reactive 主要適用於對象,包括數組和一些集合類型(如 MapSet)。對於基礎數據類型(如 stringnumberboolean),reactive 是無效的。這意味著如果你嘗試使用 reactive 來處理這些基礎數據類型,將會得到一個非響應式的對象。

import { reactive } from 'vue';
const state = reactive({ count: 0 });

使用不當會失去響應

  1. 直接賦值對象:如果直接將一個響應式對象賦值給另一個變數,將會失去響應性。這是因為 reactive 返回的是對象本身,而不僅僅是代理。

import { reactive } from 'vue';

const state = reactive({ count: 0 });
state = { count: 1 }; // 失去響應性
  1. 直接替換響應式對象:同樣,直接替換一個響應式對象也會導致失去響應性。
import { reactive } from 'vue';

const state = reactive({ count: 0 });
state = reactive({ count: 1 }); // 失去響應性
  1. 直接解構對象:在解構響應式對象時,如果直接解構對象屬性,將會得到一個非響應式的變數。
const state = reactive({ count: 0 });

let { count } = state;
count++; // count 仍然是 0
   好家伙!常用的解構賦值不能用。為瞭解決這個問題,需要使用 toRefs 函數來將響應式對象轉換為 ref 對象。
import { toRefs } from 'vue';

const state = reactive({ count: 0 });
let { count } = toRefs(state);
count++; // count 現在是 1

  首先來說,太不方便了!而且使用toRefs(),將響應式變數換成 ref 的形式,那我還不如直接使用ref()了,大家說是不是?

  1. 將響應式對象的屬性賦值給變數:如果將響應式對象的屬性賦值給一個變數,這個變數的值將不會是響應式的。

const state = reactive({ count: 0 })

let count = state.count
count++  // count 仍然是 0

使用 reactive 聲明響應式變數的確存在一些不便之處,尤其是對於喜歡使用解構賦值的開發者而言。這些局限性可能會導致意外的行為,因此在使用 reactive 時需要格外註意。相比之下,ref API 提供了一種更靈活和統一的方式來處理響應式數據。

為什麼推薦使用 ref ?

ref()它為響應式編程提供了一種統一的解決方案,適用於所有類型的數據,包括基本數據類型和複雜對象。以下是推薦使用 ref 的幾個關鍵原因:

統一性

ref 的核心優勢之一是它的統一性。它提供了一種簡單、一致的方式來處理所有類型的數據,無論是數字、字元串、對象還是數組。這種統一性極大地簡化了開發者的代碼,減少了在不同數據類型之間切換時的複雜性。

import { ref } from 'vue';

const num = ref(0);
const str = ref('Hello');
const obj = ref({ count: 0 });

// 修改基本數據類型
num.value++;
str.value += ' World';

// 修改對象
obj.value.count++;

深層響應性

ref 支持深層響應性,這意味著它可以追蹤和更新嵌套對象和數組中的變化。這種特性使得 ref 非常適合處理複雜的數據結構,如對象和數組。

import { ref } from 'vue';

const obj = ref({
  user: {
    name: 'xiaoming',
    details: {
      age: 18
    }
  }
});

// 修改嵌套對象
obj.value.user.details.age++;
當然,為了減少大型不可變數據的響應式開銷,也可以通過使用shallowRef來放棄深層響應性。
const shallowObj = shallowRef({ 
    details: { age: 18, }, 
});

靈活性

ref 提供了高度的靈活性,尤其在處理普通賦值和解構賦值方面。這種靈活性使得 ref 在開發中的使用更加方便,特別是在進行複雜的數據操作時。

import { ref } from 'vue';

const state = ref({
  count: 0,
  name: 'Vue'
});

// 解構賦值
const { count, name } = state.value;

// 直接修改解構後的變數
count++;
name = 'Vue3';

// 替換整個對象
state.value = {
  count: 10,
  name: 'Vue4'
};

總結

ref 在 Vue3 中提供了一種更統一、靈活的響應式解決方案,還能避免了 reactive 的某些局限性。希望這篇文章對你有所幫助,有所借鑒。大家怎麼認為呢,評論區我們一起討論下!

本文轉載於:

https://juejin.cn/post/7329539838776246272

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 概述:在C#多線程編程中,合理終止線程是關鍵挑戰。通過標誌位或CancellationToken,實現安全、協作式的線程終止,確保在適當時機終止線程而避免資源泄漏。 應用場景: 在C#多線程編程中,有時需要終止正在運行的線程,例如在用戶取消操作、程式關閉等情況下。 思路: 線程終止通常涉及到合作式終 ...
  • internal class Program { static List<string> list=new List<string>() { "A","B","C","D","A","B","C","D" }; static string hiddenEle1 = string.Empty;//第一 ...
  • 痞子衡嵌入式半月刊: 第 92 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • 索引:為了提高數據查詢的效率,就像書的目錄一樣。 索引的常見模型 哈希表 圖中,User2 和 User4 根據身份證號算出來的值都是 N,後面還跟了一個鏈表。假設,這時候你要查 ID_card_n2 對應的名字是什麼,處理步驟就是:首先,將 ID_card_n2 通過哈希函數算出 N;然後,按順序 ...
  • 03 事務隔離 事務:保證一組資料庫操作,要麼全部成功,要麼全部失敗。在 MySQL 中,事務支持是在引擎層實現的。 事務ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、持久性)。 建議你儘量不要使用長事務。**** 讀未提交 ...
  • 引言 在實際業務開發中,隨著業務的變化,數據的複雜性和多樣性不斷增加。傳統的關係型資料庫模型在這種情況下會顯得受限,因為它們需要預先定義嚴格的數據模式,並且通常只能存儲具有相同結構的數據。而面對非結構化或半結構化數據的存儲和處理需求,選擇使用非關係型資料庫或者創建子表存儲這些變化的結構可能會變得複雜 ...
  • 在項目開發中需要添加webview,載入內置的html文件,代碼寫完後ios運行沒有問題,運行安卓時報錯,錯誤提示如下: FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':a ...
  • 玩轉 CMS2 上篇研究了樣式、請求、evn、mock,感覺對效率的提升沒有太明顯作用。 比如某個工作需要2天,現在1天可以幹完,這就是很大的提升。 提高效率的方法有代碼復用、模塊化、低代碼工具。 目前可以考慮從代碼復用方面下手,即使最低級的代碼複製也可以。 要快速提高效率,需要對本地項目中的一些關 ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...