iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
Vue.js

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

Day 18 - Reactive:深入 Proxy 的設計思路

  • 分享至 

  • xImage
  •  

banner

在之前的文章中,我們已經完成了 ref 實作,它能將原始值包裝成響應式物件。現在,我們要接續完成另一部分的響應式系統核心:reactive 函式。我們的目標是接收一個完整的物件,並回傳一個代理物件,使其所有屬性都具備響應性。

目標設定

我們的目標很明確:完成一個 reactive 函式,讓行為跟 Vue 的官方範例一樣。

環境建置

// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'

const state = reactive({
  a: 0
})
effect(() => {
  console.log(state.a)
})

setTimeout(() => {
  state.a = 1
}, 1000)

我們期待初始化頁面輸出 0,一秒鐘後輸出1。

用註解的官方的範例,我們很明顯看到輸出值。

day18-01

我們先在src底下新增一個 reactive.ts

export function reactive(target){
}

並且在 index.ts 引入

export * from './ref'
export * from './effect'
export * from './reactive'

另外我們在 shared/src/index.ts 存放工具函式這邊寫一個物件判斷函式。

export function isObject(value) {
  return typeof value === 'object' && value !== null
}

核心思路

我們再另外寫一個函式createReactiveObject,我們實際的邏輯並不在 reactive函式中。

主要是createReactiveObject之後其他地方會用到,像是 shallowReactive 之類的。

export function reactive(target){
  return createReactiveObject(target)
}

接下來思考 createReactiveObject他本身的限制,以及我們的需求,

  1. 他只能傳入物件類型,所以我們要去判斷他的型別。
  2. reactive 的核心是用一個 Proxy 物件來處理。
  3. Proxy 的物件中會需要 get 和 set 處理收集依賴、觸發更新。
  • 收集依賴:target 本身就是依賴,因此我們需要在收集依賴時,把 targeteffect(也就是sub)建立關聯關係。
  • 觸發更新:通知之前收集的依賴,重新執行。

為什麼 Vue 3 的 reactive() 特別適合使用 Proxy?

主要是因為有幾個特性

  • Proxy 可以攔截並自定義物件的各種操作,不只是屬性的讀取和設置
  • 與 Vue 2 使用 Object.defineProperty() 相比,Proxy 的最大優勢是可以偵測新增的屬性
  • Proxy 可以直接攔截陣列的索引操作和 length 變更
  • Proxy 可以處理 MapSetWeakMapWeakSet 等集合類型

看來針對物件類型的 reactiveProxy 物件的確是一個更好的解決方案,那我們開始實作!

初步實作 - 借鏡 Ref 實作

import { isObject } from '@vue/shared'

function createReactiveObject(target){
  // reactive 只處理物件
  if(!isObject(target)) return target

  // 建立 target 的代理物件
  const proxy = new Proxy(target, {
    get(target, key){
      // 收集依賴:綁定target的屬性與effect的關係
      console.log(target, key)
      return Reflect.get(target, key)
    },
    set(target, key, newValue){
      // 觸發更新:通知之前收集的依賴,重新執行effect
      console.log(target, key, newValue)
      return Reflect.set(target, key, newValue)
    }
  })

  return proxy
}

我們來看一下,實際上的輸出值:

day18-02

看來好像蠻接近的,但依照我們寫 ref 的經驗,我們還需要做鏈表相關邏輯。

先回顧一下我們的 ref 之前怎麼寫的:

export function trackRef(dep) {
  if (activeSub) {
    link(dep, activeSub)
  }
}

export function triggerRef(dep) {
  if (dep.subs) {
    propagate(dep.subs)
  }
}
  • get有一個trackRef函式,trackRef函式判斷是不是有effect(activeSub),有的話將依賴(dep)以及effect(activeSub)傳入link函式跟做鏈表關聯關係。
  • set有一個 triggerRef函式,triggerRef函式判斷是不是收集的依賴有effect,有的話就傳入propagate作觸發更新。

看來這個依賴(dep)很重要,那什麼是依賴?

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

  subs: Link
  subsTail: Link
  constructor(value) {
    this._value = value
  }

  get value() {
    if (activeSub) {
      trackRef(this)
    }
    return this._value
  }

  set value(newValue) {
    this._value = newValue
    triggerRef(this)
  }
}

我們可以看到傳入只有

  • sub
  • subsTail

那我們可以認定只要有這兩個屬性,他就是一個 dep,那我們可以建立一個 Dep 類別,其他照 ref 的 trackRef 和 triggerRef 邏輯複製過來,並修改。

import { activeSub } from './effect'
import { link, propagate, Link } from './system'

function createReactiveObject(target){
  // reactive 只處理物件
  if(!isObject(target)) return target

  // 建立 target 的代理物件
  const proxy = new Proxy(target, {
    get(target, key){
      // 收集依賴:綁定target的屬性與effect的關係
      track(target, key)
      return Reflect.get(target, key)
    },
    set(target, key, newValue){
      // 觸發更新:通知之前收集的依賴,重新執行effect
      trigger(target, key)
      return Reflect.set(target, key, newValue)
    }
  })

  return proxy
}

class Dep{
  subs: Link
  subsTail: Link
  constructor
}

function track(target, key){
  if(!activeSub)return
  link(dep, activeSub) // 有問題
}

function trigger(target, key){
  if (dep.subs) {
    propagate(dep.subs) // 有問題
  }
}

這邊有個地方要注意,觸發通知的話要先更新數值,再去通知重新執行,所以 set 這邊要這樣寫:

set(target, key, newValue){
  const res = Reflect.set(target, key, newValue)
  // 觸發更新:通知之前收集的依賴,重新執行effect
  trigger(target, key)
  return res
}

名稱重複,調整一下 system.ts interface 名稱。

interface Dependency {
  subs: Link | undefined
  subsTail: Link | undefined
}

export interface Link {
  ...
  dep: Dependency
  ...
}

感覺新建一個Dep類別的實例,傳進 track就可以了,不過使用者傳入的 target 物件跟我們的新建的Dep似乎沒有關係。

看起來我們遇到了一些問題:

  • 我們不能再用一個 Dep 來管理所有依賴,必須為物件的每個屬性都維護一個 Dep。
  • 如何建立 target.a → Dep for a 的對應關係?
  • 如何在不污染原始 target 物件的情況下,儲存 target、key 與 Dep 之間的關聯?

為了解決這個問題,我們需要引入一個更複雜的資料結構來儲存,明天我們再接續探討。


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


上一篇
Day 17 - 效能處理:無限循環
下一篇
Day 19 - Reactive:reactive 的基礎實作
系列文
從零到一打造 Vue3 響應式系統20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言