阿里云主机折上折
  • 微信号
Current Site:Index > The principles of Reactive and Ref

The principles of Reactive and Ref

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

Reactive vs Ref Principles

Vue.js's reactivity system is one of its core features, allowing developers to declaratively describe the relationship between the UI and data. reactive and ref are the two primary ways to create reactive data in Vue 3. While both ultimately achieve reactive data tracking, they differ significantly in their underlying implementation and usage scenarios.

Basic Principles of Reactive

The reactive function takes a plain object and returns a Proxy wrapper for that object. Proxy is an ES6 feature that allows intercepting fundamental operations on the target object. Vue 3 uses Proxy's get and set traps to implement dependency tracking and update triggering.

const obj = reactive({ count: 0 })

// Simplified Proxy implementation
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key) // Dependency collection
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key) // Trigger updates
      return result
    }
  })
}

When accessing obj.count, the Proxy's get trap executes, and Vue records the currently running effect (side-effect function). When modifying obj.count, the set trap triggers, and Vue finds all effects that depend on this property and re-executes them.

Basic Principles of Ref

ref is primarily used to wrap primitive values (e.g., numbers, strings) because Proxy cannot directly proxy primitives. Ref achieves this by creating a reactive object with a value property:

const count = ref(0)

// Simplified implementation
function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newVal) {
      value = newVal
      trigger(refObject, 'value')
    }
  }
  return refObject
}

When using ref in templates, Vue automatically unwraps it, so .value is not needed. However, in JavaScript, .value must be used:

// In templates
<template>
  <div>{{ count }}</div> <!-- Auto-unwrapping -->
</template>

// In JS
console.log(count.value) // Requires .value

Comparison of Differences

  1. Data Type Handling:

    • reactive only accepts object types
    • ref accepts any type, including primitives and objects
  2. Access Method:

    • reactive objects access properties directly
    • ref requires accessing via .value
  3. Replacing Entire Objects:

    • reactive loses reactivity if replaced directly
    • ref can be replaced entirely because reactivity is bound to the ref object itself
// Reactive limitation
const state = reactive({ count: 0 })
state = { count: 1 } // Loses reactivity

// Ref flexibility
const stateRef = ref({ count: 0 })
stateRef.value = { count: 1 } // Maintains reactivity

Workflow of the Reactivity System

  1. Dependency Collection Phase: When a component renders, the render function executes. Accessing reactive data triggers the getter, registering the currently executing effect (render function) as a dependency.

  2. Update Triggering Phase: When reactive data changes, the setter triggers, notifying all dependent effects to re-execute, thereby updating the UI.

// Simplified effect implementation
let activeEffect
function effect(fn) {
  activeEffect = fn
  fn() // Execution triggers getters, collecting dependencies
  activeEffect = null
}

// Dependency collection
const depsMap = new WeakMap()
function track(target, key) {
  if (!activeEffect) return
  
  let dep = depsMap.get(target)
  if (!dep) {
    dep = new Map()
    depsMap.set(target, dep)
  }
  
  let effects = dep.get(key)
  if (!effects) {
    effects = new Set()
    dep.set(key, effects)
  }
  
  effects.add(activeEffect)
}

// Trigger updates
function trigger(target, key) {
  const dep = depsMap.get(target)
  if (!dep) return
  
  const effects = dep.get(key)
  effects && effects.forEach(effect => effect())
}

Practical Use Cases

  1. Reactive Use Cases:
    • Complex object structures
    • Scenarios requiring destructuring (with toRefs)
    • Nested data structures like form objects
const form = reactive({
  user: {
    name: '',
    age: 0
  },
  preferences: {
    theme: 'light'
  }
})

// Destructuring while maintaining reactivity
const { user, preferences } = toRefs(form)
  1. Ref Use Cases:
    • Primitive values
    • Reactive variables needing reassignment
    • Template refs for DOM elements
    • Composition function return values
// DOM reference
const inputRef = ref(null)

// Composition function
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

Performance Considerations

  1. Memory Overhead:

    • reactive creates a Proxy for the entire object
    • ref creates a wrapper object for each value
  2. Access Speed:

    • reactive direct property access is slightly faster
    • ref requires an additional .value access
  3. Batch Updates: Vue processes updates asynchronously in batches. Regardless of the method used, multiple modifications will ultimately trigger only one re-render.

Advanced Reactive APIs

  1. shallowRef: Only reacts to .value reference changes, without deep tracking of internal values.
const shallow = shallowRef({ count: 0 })
shallow.value.count++ // Won't trigger updates
shallow.value = { count: 1 } // Will trigger updates
  1. readonly: Creates a read-only reactive proxy. Any modification attempts will fail.
const ro = readonly({ count: 0 })
ro.count++ // Warning and failure
  1. customRef: Creates a custom ref, allowing control over dependency tracking and update triggering timing.
function debouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

Comparison with Vue 2

  1. Limitations of Object.defineProperty:

    • Cannot detect property addition/deletion
    • Requires special handling for array mutation methods
    • Needs recursive traversal of all object properties
  2. Advantages of Proxy:

    • Can intercept more operations (e.g., in, delete)
    • Better performance, reactive on demand
    • Supports new collection types like Map and Set
// Vue 2's reactivity implementation example
function defineReactive(obj, key) {
  let value = obj[key]
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    get() {
      dep.depend() // Collect dependencies
      return value
    },
    set(newVal) {
      value = newVal
      dep.notify() // Trigger updates
    }
  })
}

Common Issues and Solutions

  1. Reactivity Loss: Destructuring a reactive object causes reactivity loss. Use toRefs instead.
const state = reactive({ count: 0 })

// Wrong way - loses reactivity
let { count } = state

// Correct way
let { count } = toRefs(state)
  1. Circular References: Reactive handles circular references automatically but beware of infinite recursion.
const obj = reactive({})
obj.self = obj // Allowed but not recommended
  1. Primitive Value Wrapping: Directly unwrapping a ref into a reactive object maintains reactivity linkage.
const count = ref(0)
const state = reactive({ count })

count.value++ // state.count also updates

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

如果侵犯了你的权益请来信告知我们删除。邮箱: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 ☕.