iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Vue.js

從零到一打造 Vue3 響應式系統系列 第 4

Day 4 - 核心概念:收集依賴、觸發更新

  • 分享至 

  • xImage
  •  

banner

const count = ref(0)

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

setTimeout(() => {
  count.value++
}, 1000)

昨天我們的目標是讓一段簡單的 refeffect 程式碼能夠自動響應。

  1. 進入頁面輸出 count.value ==> 0
  2. 一秒後自動輸出 count.value ==> 1

然而,我們初次實作遇到問題:無法正確取值(undefined),也無法在值變更後觸發更新。

為了解決這個問題,我們要去思考 ref 需要做的事:

  1. 當取得值,ref 要怎麼知道誰在讀取?
  2. 觸發更新之後,ref 要怎麼知道要通知誰?

讓 Ref 知道誰在讀取

// 原本的程式碼
class RefImpl {
  _value;
  constructor(value){
    this._value = value
  }
}

現在要加入 getter 和 setter,讓 count.value 能正常運作:

class RefImpl {
  _value;
  
  constructor(value){
    this._value = value
  }
  
  // 新增 getter:讀取 value 時觸發
  get value(){
    console.log('有人讀取了 value!')
    return this._value
  }
  
  // 新增 setter:設定 value 時觸發
  set value(newValue){
    console.log('有人修改了 value!')
    this._value = newValue
  }
}

day4-01

現在看起來 count.value 可以正常返回值,但這個時候還是不知道讀取誰、通知誰。

Effect 函式

export function effect(fn){
  fn()
}

這時候我們需要儲存當前執行的 effect 函式。

//effect.ts
// 用來保存目前現在正在執行的 effect 函式
export let activeSub;

export function effect(fn){
  activeSub = fn
  activeSub()
  activeSub = undefined
}

這個新版的 effect 函式做了三件事:

  1. 註冊 Side Effect : 在執行傳入的函式 fn之前,先將它賦值給全域變數activeSub
  2. 執行 Side Effect: 立即執行 fn()。如果在執行過程中讀取了某個 ref.value,這個 ref就能透過activeSub 知道是誰在讀取它。
  3. 清除 Side Effect: 執行完畢後,必須將 activeSub清空 (設為undefined)。這非常重要,它能確保只有在 effect的執行期間,讀取ref的行為才會被視為依賴收集。

收集依賴實作

現在我們要讓 ref 能夠:

  1. 在被讀取時,記錄是誰在讀取(依賴收集)
  2. 在被修改時,通知所有讀取者(觸發更新)

我們可以在 getter 在讀取值的時候,判斷activeSub是否存在,來確認當下情況是不是要收集依賴。

// ref.ts
import { activeSub } from './effect'

class RefImpl {
  _value;
  subs;  // 新增:用來儲存訂閱者
  
  constructor(value){
    this._value = value
  }
  
  // 新增 getter:讀取 value 時觸發
  get value(){
    // 依賴收集:如果有 activeSub,就記錄下來
    if(activeSub){
      this.subs = activeSub
    }
    return this._value
  }
  
  // 新增 setter:設定 value 時觸發
  set value(newValue){
    // 觸發更新:如果有訂閱者,就執行它
    if(this.subs){
      this.subs()  // 重新執行 effect
    } // 可簡寫 this.subs?.()
  }
}

為了方便在後續的系統中判斷一個變數是否為ref物件,我們可以新增一個輔助函式 isRef 和一個內部標記:

enum ReactiveFlags {
  IS_REF = '__v_isRef'
}

class RefImpl {
  _value;
  subs;  // 新增:用來儲存訂閱者
  [ReactiveFlags.IS_REF] = true
  
  ...
}

export function isRef(value){
  return !!(value && value[ReactiveFlags.IS_REF])
}

現在,讓我們將所有部分串連起來,完整地模擬執行流程。


完整流執行流程

頁面初始化與依賴收集

剛開始進入頁面。

import { ref, effect } from '../dist/reactivity.esm.js'

const count = ref(0)

程式執行:const count = ref(0)

  • 執行 ref(0),建立一個 RefImpl 實例。
  • 此時 count 實例的內部狀態為:
    • _value: 0
    • 沒有任何訂閱者:subs: undefined
    • 帶有一個內部標記:__v_isRef: true

呼叫 effect 函式,並傳入匿名函式 fn 作為參數。

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

進入 effect 函式內部


export let activeSub;

export function effect(fn){
  activeSub = fn
  activeSub()
  activeSub = undefined
}
  1. 設定 activeSub: activeSub 被賦值為 fnactiveSub = fn

  2. 立刻執行 fn()

    1. 執行 console.log('effect', count.value)
    2. 觸發了 count 實例的 get value()
    3. 進入 getter 內部:
    • if(activeSub) 條件成立,activeSub 正是我們的 fn

      if(activeSub){
         this.subs = activeSub
      }
      
    1. 執行「收集依賴」:this.subs = activeSub
    2. 現在 count 實例透過 subs 屬性,記住了是 fn 在依賴它。
    3. getter 回傳 this._value(也就是 0)。
    4. console.log 輸出:effect 0
  3. activeSub = undefined(執行完成後清空,沒有 effect 在執行)。

此時

  1. count.subs 就是傳入 effect 的函式。
  2. 依賴關係:counteffect(fn)

一秒之後

  • set value(newValue) 被呼叫,this._value = 1
  • this.subs?.() 若有訂閱者就呼叫(這裡就是前面存起來的 effect 函式)
  • 觸發更新 effect 函式再次執行
    • console.log('effect', count.value) → 讀 getter → 看見沒有 activeSub,所以不會收集依賴。
    • 這會直接執行 effect 函式本體,不是再經過 effect(fn) 的包裝流程,所以第二次之後執行 effect 時 activeSubundefined
    • console.log 輸出:effect 1

這樣我們就完成響應式依賴收集的最小可行版本。


完整程式碼

ref.ts

import { activeSub } from './effect'

enum ReactiveFlags {
  IS_REF = '__v_isRef'
}

class RefImpl {
  _value; // 保存實際數值
   // ref 標記,證實是個 ref
  [ReactiveFlags.IS_REF] = true

  subs
  constructor(value){
    this._value = value
  }

  // 收集依賴
  get value(){ 
    // 當有人訪問的時候,可以取得 activeSub
    if(activeSub){
      //當有 activeSub 儲存值,以便更新後觸發
      this.subs = activeSub
    }
    return this._value
  }

  // 觸發更新
  set value(newValue){ 
    this._value = newValue
    // 通知 effect 重新執行,取得最新的 value
    this.subs?.()
  }
}

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

export function isRef(value){
  return !!(value && value[ReactiveFlags.IS_REF])
}

effect.ts

// 用來保存目前現在正在執行的 effect 函式
export let activeSub;

export function effect(fn){
  activeSub = fn
  activeSub()
  activeSub = undefined
}

同步更新《嘿,日安!》技術部落格


上一篇
Day 3 - 核心概念: 從「訂閱者模式」看響應式設計
下一篇
Day 5 - 核心概念:單向鏈表、雙向鏈表
系列文
從零到一打造 Vue3 響應式系統10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言