阿里云主机折上折
  • 微信号
Current Site:Index > Refactoring the reactive system (replacing defineProperty with Proxy)

Refactoring the reactive system (replacing defineProperty with Proxy)

Author:Chuan Chen 阅读数:30888人阅读 分类: Vue.js

Reactive System Refactoring (Proxy Replacing defineProperty)

Vue.js 2.x uses Object.defineProperty to implement its reactive system, while Vue 3 refactors the entire reactivity mechanism using Proxy. This change brings performance improvements and enhanced functionality while also addressing some inherent limitations of defineProperty.

Limitations of defineProperty

Object.defineProperty has several key issues:

  1. Cannot detect property addition or deletion:
const obj = {}
Object.defineProperty(obj, 'a', {
  get() { return this._a },
  set(val) { 
    console.log('set a')
    this._a = val 
  }
})
obj.a = 1 // triggers set
obj.b = 2 // no reactivity triggered
  1. Array mutation methods require special handling:
const arr = []
Object.defineProperty(arr, 'length', { /* ... */ })
arr.push(1) // does not trigger length setter
  1. Significant performance overhead, as it requires recursively traversing all object properties for conversion.

Advantages of Proxy

ES6's Proxy perfectly solves these issues:

const handler = {
  get(target, key) {
    console.log(`Get ${key}`)
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    console.log(`Set ${key} = ${value}`)
    return Reflect.set(target, key, value)
  },
  deleteProperty(target, key) {
    console.log(`Delete ${key}`)
    return Reflect.deleteProperty(target, key)
  }
}

const proxy = new Proxy({}, handler)
proxy.a = 1 // outputs "Set a = 1"
proxy.a     // outputs "Get a"
delete proxy.a // outputs "Delete a"

Vue 3's Reactive Implementation

Core implementation of Vue 3's reactive:

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      track(target, key) // dependency collection
      const res = Reflect.get(target, key, receiver)
      if (typeof res === 'object' && res !== null) {
        return reactive(res) // deep reactivity
      }
      return res
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        trigger(target, key) // trigger updates
      }
      return result
    }
  }
  return new Proxy(target, handler)
}

Performance Comparison

  1. Initialization performance:

    • defineProperty: Requires recursive traversal of all properties
    • Proxy: Processes only on access (lazy conversion)
  2. Memory usage:

    • defineProperty: Requires creating closures for each property to store dependencies
    • Proxy: Shares a single handler for the entire object
  3. Array handling:

const arr = reactive([1, 2, 3])
arr.push(4) // triggers reactivity normally
arr.length = 10 // also triggers reactivity

Practical Use Cases

  1. Dynamic properties:
const state = reactive({})
// Can dynamically add reactive properties
state.newProp = 'value' 
  1. Map/Set support:
const map = reactive(new Map())
map.set('key', 'value') // reactive update
  1. Better TypeScript support:
interface State {
  count: number
  user?: {
    name: string
  }
}
const state: State = reactive({
  count: 0
})
// Type-safe access
state.count++

Compatibility Considerations

Although Proxy is a modern browser feature, Vue 3 provides a fallback solution:

function createReactive(target) {
  return ProxySupported 
    ? new Proxy(target, handler)
    : fallbackDefineProperty(target)
}

For scenarios requiring IE support, the @vue/compat compatibility build can be used.

Evolution of Reactive APIs

Vue 3 introduces a series of new reactive APIs:

import { reactive, ref, computed, watchEffect } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)
const state = reactive({ list: [] })

watchEffect(() => {
  console.log(`count is ${count.value}`)
})

Comparison with Vue 2

  1. Array detection:
// Vue 2
this.$set(this.arr, index, value)

// Vue 3
arr[index] = value // direct assignment works
  1. Nested objects:
// Vue 2
this.$set(this.obj, 'nested', {})

// Vue 3
obj.nested = {} // automatically reactive

Source Code Analysis

Simplified dependency collection implementation:

const targetMap = new WeakMap()

function track(target, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(effect => effect.run())
}

Best Practices

  1. Handling large objects:
// Avoid making large objects reactive at once
const bigData = shallowReactive({ /* large dataset */ })
  1. Performance-sensitive scenarios:
// Use markRaw to skip reactive conversion
import { markRaw } from 'vue'
const nonReactive = markRaw({ shouldNotTrack: true })
  1. Composable functions:
function useCounter() {
  const count = ref(0)
  const double = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  return { count, double, increment }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.