阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > Reactive与Ref原理

Reactive与Ref原理

作者:陈川 阅读数:7904人阅读 分类: Vue.js

Reactive与Ref原理

Vue.js的响应式系统是其核心特性之一,它允许开发者声明式地描述UI与数据之间的关系。reactiveref是Vue3中创建响应式数据的两种主要方式,虽然它们最终都实现了数据的响应式追踪,但在底层实现和使用场景上存在明显差异。

reactive的基本原理

reactive函数接收一个普通对象,返回该对象的Proxy代理。Proxy是ES6引入的特性,允许拦截对目标对象的底层操作。Vue3通过Proxy的get和set陷阱实现依赖收集和触发更新。

const obj = reactive({ count: 0 })

// 底层Proxy实现示意
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key) // 依赖收集
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key) // 触发更新
      return result
    }
  })
}

当访问obj.count时,Proxy的get陷阱会执行,Vue会记录当前正在运行的effect(副作用函数)。当修改obj.count时,set陷阱触发,Vue会找到所有依赖这个属性的effect并重新执行它们。

ref的基本原理

ref主要用于包装基本类型值(如number、string等),因为Proxy无法直接代理基本类型。ref通过创建一个包含value属性的响应式对象来实现:

const count = ref(0)

// 简化实现
function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newVal) {
      value = newVal
      trigger(refObject, 'value')
    }
  }
  return refObject
}

在模板中使用ref时,Vue会自动解包,不需要写.value。但在JavaScript中必须通过.value访问:

// 模板中
<template>
  <div>{{ count }}</div> <!-- 自动解包 -->
</template>

// JS中
console.log(count.value) // 需要.value

两者差异比较

  1. 数据类型处理

    • reactive只接受对象类型
    • ref可以接受任何类型,包括基本类型和对象
  2. 访问方式

    • reactive对象直接访问属性
    • ref需要通过.value访问
  3. 替换整个对象

    • reactive直接替换会失去响应性
    • ref可以整体替换,因为响应性绑定在ref对象本身
// reactive限制
const state = reactive({ count: 0 })
state = { count: 1 } // 失去响应性

// ref灵活性
const stateRef = ref({ count: 0 })
stateRef.value = { count: 1 } // 保持响应性

响应式系统的工作流程

  1. 依赖收集阶段: 当组件渲染时,会执行render函数,访问响应式数据时会触发getter,将当前正在执行的effect(渲染函数)注册为依赖。

  2. 触发更新阶段: 当响应式数据变化时,setter会触发,通知所有依赖的effect重新执行,实现UI更新。

// 简化的effect实现
let activeEffect
function effect(fn) {
  activeEffect = fn
  fn() // 执行过程中会触发getter,收集依赖
  activeEffect = null
}

// 依赖收集
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)
}

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

实际应用场景

  1. reactive适用场景
    • 复杂对象结构
    • 需要解构使用的场景(配合toRefs)
    • 表单对象等嵌套数据结构
const form = reactive({
  user: {
    name: '',
    age: 0
  },
  preferences: {
    theme: 'light'
  }
})

// 解构保持响应性
const { user, preferences } = toRefs(form)
  1. ref适用场景
    • 基本类型值
    • 需要重新赋值的响应式变量
    • 模板ref引用DOM元素
    • 组合式函数返回值
// DOM引用
const inputRef = ref(null)

// 组合式函数
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

性能考量

  1. 内存开销

    • reactive为整个对象创建Proxy
    • ref为每个值创建包装对象
  2. 访问速度

    • reactive直接访问属性略快
    • ref需要多一层.value访问
  3. 批量更新: Vue会异步批量处理更新,无论使用哪种方式,多次修改最终只会触发一次重新渲染。

响应式API进阶

  1. shallowRef: 只对.value的引用变化响应,不深度追踪内部值。
const shallow = shallowRef({ count: 0 })
shallow.value.count++ // 不会触发更新
shallow.value = { count: 1 } // 会触发更新
  1. readonly: 创建只读的响应式代理,任何修改尝试都会失败。
const ro = readonly({ count: 0 })
ro.count++ // 警告并失败
  1. customRef: 创建自定义的ref,可以控制依赖追踪和触发更新的时机。
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)
      }
    }
  })
}

与Vue2的对比

  1. Object.defineProperty的局限

    • 无法检测对象属性的添加/删除
    • 对数组的变异方法需要特殊处理
    • 需要递归遍历对象的所有属性
  2. Proxy的优势

    • 可以拦截更多操作(如in、delete等)
    • 性能更好,按需响应
    • 支持Map、Set等新集合类型
// Vue2的响应式实现示意
function defineReactive(obj, key) {
  let value = obj[key]
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    get() {
      dep.depend() // 收集依赖
      return value
    },
    set(newVal) {
      value = newVal
      dep.notify() // 触发更新
    }
  })
}

常见问题与解决方案

  1. 响应式丢失: 解构reactive对象会导致响应式丢失,需要使用toRefs
const state = reactive({ count: 0 })

// 错误方式 - 响应式丢失
let { count } = state

// 正确方式
let { count } = toRefs(state)
  1. 循环引用: reactive处理循环引用时会自动处理,但要注意无限递归的情况。
const obj = reactive({})
obj.self = obj // 允许但不推荐
  1. 原始值包装: 直接解包ref到reactive对象中会保持响应性连接。
const count = ref(0)
const state = reactive({ count })

count.value++ // state.count也会更新

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

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌