記錄--源碼視角,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 MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...