深入 vue3 源碼解讀 -- 1、響應式的基礎概念

🧭 學習主線

讀取響應式資料時收集依賴,修改響應式資料時觸發依賴重新執行。

可以把它拆成三個關鍵角色:

角色 作用 對應程式碼
📦 響應式資料 保存值,並攔截讀取和修改 ref / RefImpl
🧲 依賴收集 在讀取 .value 時記錄誰用到了這個值 get value()
🔁 觸發更新 在修改 .value 時重新執行依賴函式 set value()

🧠 1. 什麼是響應式

Vue 的響應式系統核心在於:響應式物件的屬性與 effect 副作用函式之間建立依賴關係。

🔹 1.1 一般函式存取響應式資料

💡 原始碼理解

一般函式裡雖然讀取了 count.value,但是 Vue 並不知道這個函式之後需要被重新執行。

原因是:這次讀取沒有處在 effect 的收集環境裡,所以 count 沒有機會把這個函式記錄下來。後面就算 count.value 變了,也找不到要重新執行的函式。

javascript 代碼解讀複製代碼import { ref } from 'vue'
const count = ref(0)

// 一般函式
function fn(){
  console.log(count.value)
}

fn() // 印出 0

setTimeout(()=>{
  count.value = 1 // 修改值不會觸發 fn 重新執行
})

雖然 fn 讀取了響應式資料 count.value,但由於它不是在 effect 中執行的,因此當 count.value 發生變化時,該函式不會重新執行。

✅ 這裡要記住

ref 本身只是讓資料具備「可追蹤」的能力,但是否真的追蹤,還要看讀取發生在哪裡。


🔹 1.2 在 effect 中存取響應式資料

💡 原始碼理解

effect 的作用可以先簡單理解成:告訴響應式系統「這個函式是需要被追蹤的」。

effect 內部讀取 count.value 時,count 就可以把目前正在執行的函式儲存起來。等後面 count.value 被修改時,再把這個函式拿出來重新執行。

javascript 代碼解讀複製代碼import { ref, effect } from 'vue'

const count = ref(0)

effect(()=>{
  console.log(count.value) // 首次執行印出 0
})

setTimeout(()=>{
  count.value = 1 // 觸發 effect 重新執行,印出 1
})

🔁 執行流程

  1. effect 先執行一次傳入的函式
  2. 函式執行時讀取 count.value
  3. 觸發 get value()
  4. setTimeout 中修改 count.value
  5. 觸發 set value()
  6. 通知依賴重新執行

🛠️ 2、在原始碼中實作

📁 1、在 reactivity/src 中新建三個 ts 檔案

📄 1、新建 effect.ts

💡 原始碼理解

第一版 effect 先不要想得太複雜,它最基礎的能力就是:接收一個函式,並立即執行它。

這一步只是搭建入口,後面才會繼續給它加上「目前正在執行的 effect」這個狀態。

php 代碼解讀複製代碼export function effect(fn){
  fn()
}

📄 2、新建 index.ts

💡 原始碼理解

index.ts 的作用是統一出口。

以後外部使用時,不需要分別去找 ref.tseffect.ts,只要從目前模組入口匯入即可。

javascript 代碼解讀複製代碼export * from './ref'
export * from './effect'

📄 3、新建 ref.ts

💡 原始碼理解

ref(value) 的本質不是直接回傳原始值,而是把原始值包一層物件。

這樣做的原因是:只有包成物件之後,才可以透過 get value()set value() 攔截讀取與修改。

javascript 代碼解讀複製代碼class RefImpl{
  constructor(value) {
    this._value = value
  }
}

export function ref(value){
  return new RefImpl(value)
}

✅ 這裡要記住

count 不是數字 0,而是一個 RefImpl 實例;真正的值被放在 _value 裡。


📁 2、在 reactivity/src 同級新建 examples 資料夾(存放測試案例)

📄 1、新建 01-demo.html

✅ 1、1 在 Vue 中實作 1s 之後印出 1

💡 原始碼理解

這個案例先用 Vue 官方的 refeffect 跑通效果,目的是給後面自己實作原始碼一個對照目標。

學習原始碼時不要一上來就寫實作,先確認最終行為是什麼:首次印出一次,1 秒後值變化,再印出一次。

xml 代碼解讀複製代碼<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.prod.js'

      const count = ref(0)

      effect(() => {
        console.log('effect1 count.value =>', count.value)
      })

      setTimeout(() => {
        count.value = 1
      }, 1000)
    </script>
  </body>
</html>

✅ 1、2 我們如何去實作?

逐步去完善 reactivity/src 的 ts 檔案


📄 1、ref.ts

💡 原始碼理解

這一版 ref.ts 開始具備響應式的核心雛形:

程式碼位置 作用
_value 保存真實的值
[ReactiveFlgs.IS_REF] = true 為目前物件打上 ref 標記
get value() 讀取 .value 時觸發
set value() 修改 .value 時觸發
isRef 判斷一個值是不是 ref

這裡的 console.log(' 有人訪問我了 ')console.log(' 我的值變了 ') 只是為了觀察流程。真正的原始碼裡,這兩個位置分別會做「依賴收集」和「觸發更新」。

javascript 代碼解讀複製代碼enum ReactiveFlgs = {
  IS_REF = '__v_isRef' // ref 標記,證明是個 ref
}

/*
 * Ref 的類別
 * */
class RefImpl{
  // 保存實際的值
  _value;

  // ref 標記,證明是個 ref
  [ReactiveFlgs.IS_REF] = true

  constructor(value) {
    this._value = value
  }

  get value(){
    // 收集依賴
    console.log(' 有人訪問我了 ')
    return this._value;
  }

  set value(newValue){
    // 觸發更新
    console.log(' 我的值變了 ')
    this._value = newValue
  }
}

/*
 * 判斷是不是一個 ref
 * @params value
 * */
export function isRef(value){
  return !!(value && value[ReactiveFlgs.IS_REF])
}

export function ref(value){
  return new RefImpl(value)
}

✅ 這裡要記住

響應式不是值自己會動,而是讀取和修改這兩個動作被攔截了。


❓ 如何去收集依賴?如何去觸發更新???

🧩 原始碼思路拆解

要讓 count.value = 1 後重新執行 effect,需要解決兩個問題:

問題 解決方式
讀取時怎麼知道是誰在讀? 用一個全域變數保存目前正在執行的 effect
修改時怎麼知道通知誰? 在 ref 實例上保存之前收集到的 effect

所以整體思路是:

  1. effect(fn) 執行前,把 fn 標記成目前活躍的副作用函式
  2. 執行 fn
  3. fn 內部讀取 count.value
  4. get value() 發現目前有活躍的 effect,就把它保存到 subs
  5. 修改 count.value
  6. set value() 執行 subs

📄 effect.ts

💡 原始碼理解

activeSub 可以理解成一個暫存變數,用來保存「目前正在被收集的函式」。

為什麼需要這個變數?因為 get value() 觸發的時候,它本身並不知道是誰讀取了 .value。所以需要 effect 在外面先把目前函式放到一個共用位置,get value() 再從這個位置取到它。

scss 代碼解讀複製代碼// 用來保存目前正在執行的 effect   
// 相當於範例中的
// () => {
//  console.log('count.value =>', count.value)
//  }
export let activeSub

export function effect(fn){
  activeSub = fn()
  activeSub()  // 就是執行 fn()
  activeSub = underfined
}

✅ 這裡要記住

學習這一段時重點看思想:effect 負責打開收集視窗,ref.value 的 getter 負責在視窗打開時把依賴記下來。


📄 ref.ts

💡 原始碼理解

最終這版 ref.ts 把依賴收集和觸發更新串起來了。

關鍵點在這兩處:

位置 做的事情
get value() 如果存在 activeSub,表示目前讀取發生在 effect 中,於是保存依賴
set value() 修改值之後,執行之前保存的依賴函式
javascript 代碼解讀複製代碼import { activeSub } from './effect'
enum ReactiveFlgs = {
  IS_REF = '__v_isRef' // ref 標記,證明是個 ref
}

/*
 * Ref 的類別
 * */
class RefImpl{
  // 保存實際的值
  _value;

  // ref 標記,證明是個 ref
  [ReactiveFlgs.IS_REF] = true

  // 保存和 effect 之間的關聯關係
  subs

  constructor(value) {
    this._value = value
  }

  get value(){
    // 收集依賴
    if(activeSub){
      // 如果 activeSub 有,保存起來,等我更新時觸發
      this.subs = activeSub
    }
    return this._value;
  }

  set value(newValue){
    // 觸發更新
    this._value = newValue
    this.subs?.()  // 可選鏈 ?.  activeSub 賦值給 this.subs 可能是空的
  }
}

/*
 * 判斷是不是一個 ref
 * @params value
 * */
export function isRef(value){
  return !!(value && value[ReactiveFlgs.IS_REF])
}

export function ref(value){
  return new RefImpl(value)
}


🧠 最後總結

這一節可以先不用追求一次性還原 Vue 完整原始碼,先把最小響應式模型跑通:

階段 發生了什麼
建立 ref(0) 建立一個 RefImpl 實例
首次執行 effect 執行傳入的函式
讀取 存取 count.value,觸發 get value()
收集 get value() 把目前 effect 保存起來
修改 count.value = 1,觸發 set value()
更新 set value() 重新執行之前保存的 effect

原始碼學習時最重要的是抓住這個閉環:

effect 執行函式 → 函式讀取響應式資料 → 響應式資料收集函式 → 資料變化 → 函式重新執行


原文出處:https://juejin.cn/post/7652180405778087946


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝16   💬1   ❤️1
464
🥈
我愛JS
📝1   ❤️1
22
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登