記錄--源碼視角,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
  • 基於.NET Framework 4.8 開發的深度學習模型部署測試平臺,提供了YOLO框架的主流系列模型,包括YOLOv8~v9,以及其系列下的Det、Seg、Pose、Obb、Cls等應用場景,同時支持圖像與視頻檢測。模型部署引擎使用的是OpenVINO™、TensorRT、ONNX runti... ...
  • 十年沉澱,重啟開發之路 十年前,我沉浸在開發的海洋中,每日與代碼為伍,與演算法共舞。那時的我,滿懷激情,對技術的追求近乎狂熱。然而,隨著歲月的流逝,生活的忙碌逐漸占據了我的大部分時間,讓我無暇顧及技術的沉澱與積累。 十年間,我經歷了職業生涯的起伏和變遷。從初出茅廬的菜鳥到逐漸嶄露頭角的開發者,我見證了 ...
  • C# 是一種簡單、現代、面向對象和類型安全的編程語言。.NET 是由 Microsoft 創建的開發平臺,平臺包含了語言規範、工具、運行,支持開發各種應用,如Web、移動、桌面等。.NET框架有多個實現,如.NET Framework、.NET Core(及後續的.NET 5+版本),以及社區版本M... ...
  • 前言 本文介紹瞭如何使用三菱提供的MX Component插件實現對三菱PLC軟元件數據的讀寫,記錄了使用電腦模擬,模擬PLC,直至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1. PLC開發編程環境GX Works2,GX Works2下載鏈接 https:// ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • 1、jQuery介紹 jQuery是什麼 jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫(或JavaScript框架)。jQuery設計的宗旨是“write Less,Do More”,即倡導寫更少的代碼,做更多的事情。它封裝 ...
  • 前言 之前的文章把js引擎(aardio封裝庫) 微軟開源的js引擎(ChakraCore))寫好了,這篇文章整點js代碼來測一下bug。測試網站:https://fanyi.youdao.com/index.html#/ 逆向思路 逆向思路可以看有道翻譯js逆向(MD5加密,AES加密)附完整源碼 ...
  • 引言 現代的操作系統(Windows,Linux,Mac OS)等都可以同時打開多個軟體(任務),這些軟體在我們的感知上是同時運行的,例如我們可以一邊瀏覽網頁,一邊聽音樂。而CPU執行代碼同一時間只能執行一條,但即使我們的電腦是單核CPU也可以同時運行多個任務,如下圖所示,這是因為我們的 CPU 的 ...
  • 掌握使用Python進行文本英文統計的基本方法,並瞭解如何進一步優化和擴展這些方法,以應對更複雜的文本分析任務。 ...
  • 背景 Redis多數據源常見的場景: 分區數據處理:當數據量增長時,單個Redis實例可能無法處理所有的數據。通過使用多個Redis數據源,可以將數據分區存儲在不同的實例中,使得數據處理更加高效。 多租戶應用程式:對於多租戶應用程式,每個租戶可以擁有自己的Redis數據源,以確保數據隔離和安全性。 ...