iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Vue.js

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

Day 6 - 首次實作: 鏈表應用

  • 分享至 

  • xImage
  •  

banner

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

const count = ref(0)

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

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

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

昨天,我們了解鏈表的核心觀念,現在要把這些概念結合起來。

首先讓我們從一個常見的場景開始:當一個響應式數據(ref)同時被多個 effect 依賴時,會發生什麼?

我們預期他可以輸出如下:

console.log('effect1', 0)
console.log('effect2', 0)
//1秒後
console.log('effect1', 1)
console.log('effect2', 1)

但實際上我們得到的是:

console.log('effect1', 0)
console.log('effect2', 0)
//1秒後
console.log('effect2', 1)

發生什麼事?

結果很明顯:我們上次做的ref實作,只能讓this.subs屬性一次記住一個訂閱者,導致後來的effect覆蓋前面。這會造成以下問題:

  • 每次有新的 effect 訂閱時,會覆蓋掉前一個
  • 導致只有最後一個 effect 能收到更新通知
get value(){ 
  if(activeSub){
    this.subs = activeSub 
  }
  return this._value
}

第一個effect加入

  • 執行console.log('effect1', 0)
  • 收集依賴 effect(fn1)activeSub = fn1,然後立刻執行 fn1()
  • fn1 讀取 count.value → 進入 getter:
    • activeSub 存在 → this.subs = activeSub(把 subs 指到 fn1)。
    • 回傳 0,所以印出 effect1 0
  • effect(fn1) 結束,把 activeSub 清回 undefined

第二個effect加入

  • 執行console.log('effect2', 0)
  • 收集依賴 effect(fn2)activeSub = fn2,執行 fn2()
  • fn2count.value → getter:
    • activeSub 存在 → this.subs = activeSub 覆蓋掉 fn1,現在 subs === fn2
    • 回傳 0,印出 effect2 0
  • effect(fn2) 結束,把 activeSub 清回 undefined

一秒後更新觸發

set value(newValue){ 
    this._value = newValue
    this.subs?.()
  }
  • 執行 count.value = 1
  • 進入 setter:this._value = 1
  • 呼叫 this.subs?.()直接呼叫目前存在於 subs 的函式 fn2
  • 因為只有 fn2 被呼叫,所以只印出 console.log('effect2', 1)

問題解決方案

接下來我們運用上次說的鏈表,來處理被覆蓋的問題,這邊我們使用雙向鏈表:

//ref.ts

// 定義鏈表節點結構
interface Link {
  // 保存 effect
  sub:Function
  // 下一個節點
  nextSub:Link
  // 上一個節點
  prevSub:Link
}

class RefImpl {
  _value;
  [ReactiveFlags.IS_REF] = true

  subs:Link //訂閱者鏈表頭節點
  subsTail:Link //訂閱者鏈表尾節點

  constructor(value){
    this._value = value
  }

  get value(){ 
    if(activeSub){
      // 建立節點
      const newLink = {
        sub: activeSub,
        nextSub:undefined,
        prevSub:undefined
      }
    
    /**
      * 關聯鏈表關係
      * 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。
      * 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。 
      */
      // 
      if(this.subsTail){
        this.subsTail.nextSub = newLink
        newLink.prevSub = this.subsTail
        this.subsTail = newLink
      }else { 
        this.subs = newLink
        this.subsTail = newLink
      }
    }
    return this._value
  }

  set value(newValue ){ 
    this._value = newValue
    
    // 取得頭節點
    let link = this.subs
    let queuedEffect = []

    // 遍歷整個鏈表的每一個節點
    // 把每個節點裡的 effect 函數放進陣列
    // 不是放節點本身,是放節點裡的 sub 屬性(effect 函數)
    while (link){
      queuedEffect.push(link.sub)
      link = link.nextSub
    }

    //觸發更新
    queuedEffect.forEach(effect => effect())
  }
}

解決後執行流程

初始化

day06-01

  • 初始化,在走到 effect 之前,頭尾節點都是 undefined

第一個effect加入

day06-02

  • effect(fn1) 訪問 count
  • activeSub = effect1,馬上執行 effect1()
  • effect1 讀取 count.value → 進 get
    • activeSub 存在 → 建立 newLink(effect1)
    • 因為當前 subsTailundefined,所以把 頭節點跟尾節點都指向 newLink(effect1)
  • 輸出 effect1 0
  • 清除activeSubactiveSub = undefined

第二個effect加入

day06-03

  • effect(fn2) 訪問 count
  • activeSub = effect2,執行 effect2()
  • effect2 讀取 count.value → 觸發 getter
    • activeSub 存在 → 建立 newLink(effect2)
    • 這次 subsTail 存在(指向 effect1),所以把 effect2 掛在尾端:
      • effect1.next = effect2
      • effect2.prev = effect1
      • subsTail = effect2
  • 輸出 effect2 0
  • 清除activeSubactiveSub = undefined

一秒後更新觸發

  • 執行 count.value = 1
  • 觸發 setterthis._value = 1
  • 頭節點 開始遍歷鏈表,把每個節點的 sub(也就是 effect 函式)放進 queuedEffect
    • 先推 effect1,再推 effect2
  • queuedEffect.forEach(fn => fn()) 依序執行:
    • 先跑 effect1() → 列印 effect1 1
    • 再跑 effect2() → 列印 effect2 1

透過雙向鏈表,我們成功解決了訂閱者被覆蓋的問題。現在無論有多少個 effect 依賴,都能在資料變更時收到通知並更新。


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


上一篇
Day 5 - 核心概念:單向鏈表、雙向鏈表
下一篇
Day 7 - 關注點分離:拆分 track、trigger
系列文
從零到一打造 Vue3 響應式系統10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言