Vue3响应式原理深度解析:Proxy拦截与依赖收集机制

Vue3响应式原理深度解析:Proxy拦截与依赖收集机制

Vue3使用ES6 Proxy替代Vue2的Object.defineProperty实现响应式系统,解决了无法检测属性新增/删除和数组索引修改的局限性。理解Proxy拦截机制和依赖收集流程,对排查响应式失效问题和编写高性能组件至关重要。

Proxy代理对象的工作机制

Vue3的reactive()函数创建Proxy代理对象,拦截对目标对象的读取和修改操作:

// Vue3 reactive 简化实现
const targetMap = new WeakMap();
let activeEffect = null;

function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

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

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

function reactive(target) {
  const handler = {
    get(obj, key, receiver) {
      const result = Reflect.get(obj, key, receiver);
      track(target, key);
      // 深层代理:对象类型属性递归创建Proxy
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }
      return result;
    },
    set(obj, key, value, receiver) {
      const oldValue = obj[key];
      const result = Reflect.set(obj, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key);
      }
      return result;
    }
  };
  return new Proxy(target, handler);
}

track在属性被读取时记录当前活跃的副作用函数(activeEffect),建立属性与依赖的映射关系。trigger在属性被修改时找到对应的依赖集合并执行所有副作用函数。依赖存储结构为三层:WeakMap(target) → Map(key) → Set(effect)

ref与reactive的区别与转换

ref用于包装基本类型值,内部通过RefImpl类实现get/set拦截:

class RefImpl {
  private _value;
  private _rawValue;
  public dep;
  
  constructor(value) {
    this._rawValue = value;
    this._value = convert(value);
    this.dep = new Set();
  }
  
  get value() {
    trackRefValue(this);
    return this._value;
  }
  
  set value(newVal) {
    if (newVal !== this._rawValue) {
      this._rawValue = newVal;
      this._value = convert(newVal);
      triggerRefValue(this);
    }
  }
}

function convert(val) {
  return typeof val === 'object' && val !== null 
    ? reactive(val) 
    : val;
}

ref赋值对象类型时,内部调用reactive()转换。模板中ref自动解包(unwrap),但JavaScript代码中必须通过.value访问。watch和computed接收的值已经自动解包。

computed缓存的实现原理

computed具有缓存特性:依赖未变化时返回缓存值,避免重复计算。实现依赖dirty标志位:

class ComputedRefImpl {
  private _dirty = true;
  private _value;
  private _effect;
  
  constructor(getter) {
    this._effect = new ReactiveEffect(getter, () => {
      // 依赖变化时设置dirty,触发副作用
      if (!this._dirty) {
        this._dirty = true;
        triggerRefValue(this);
      }
    });
  }
  
  get value() {
    trackRefValue(this);
    if (this._dirty) {
      this._dirty = false;
      this._value = this._effect.run();
    }
    return this._value;
  }
  
  set value(newVal) {
    if (setter) setter(newVal);
  }
}

首次访问.value_dirty为true,执行getter计算并缓存结果。后续访问直接返回缓存。当依赖的响应式数据变化时,scheduler回调将_dirty设为true,下次访问时重新计算。多个组件引用同一个computed时,只计算一次。

watchEffect与watch的触发差异

watchEffect立即执行副作用函数,自动追踪函数内访问的响应式依赖:

// watchEffect:自动收集依赖,立即执行
const count = ref(0);
const name = ref('test');

watchEffect(() => {
  console.log(count.value, name.value);
});
// 输出:0 test

count.value = 1;
// 输出:1 test

// watch:显式指定侦听源,获取新旧值
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`);
}, { immediate: false, deep: false });

// 多源侦听
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`count: ${oldCount}→${newCount}, name: ${oldName}→${newName}`);
});

watchdeep: true选项对reactive对象执行深层遍历。深层watch会递归访问对象所有属性触发track,性能开销与对象深度成正比。对大型嵌套对象,优先使用watchEffect只追踪实际访问的属性。

响应式失效场景排查

常见响应式失效原因及解决方案:

场景1:解构reactive对象丢失响应性

const state = reactive({ count: 0, name: 'test' });

// 错误:解构后count是基本类型,失去响应性
const { count, name } = state;

// 正确方案1:使用toRefs保持响应性
const { count, name } = toRefs(state);
// count.value, name.value 仍是响应式

// 正确方案2:直接通过state访问
state.count

场景2:直接替换reactive对象

let state = reactive({ count: 0 });

// 错误:重新赋值后state指向新对象,旧Proxy被丢弃
state = reactive({ count: 1 });

// 正确方案1:使用Object.assign
Object.assign(state, { count: 1 });

// 正确方案2:改用ref
const state = ref({ count: 0 });
state.value = { count: 1 }; // ref的set会触发更新

场景3:数组操作未触发更新

const list = reactive([1, 2, 3]);

// Vue3中Proxy能正确拦截数组方法,以下操作均有效:
list.push(4);          // 触发更新
list.splice(0, 1);     // 触发更新
list[0] = 99;          // 触发更新(Vue2中无效)
list.length = 0;       // 触发更新(Vue2中无效)

性能优化:shallowRef与shallowReactive

深层响应式代理会对对象递归创建Proxy,大型数据结构(如10000条记录的列表)初始化时产生明显开销。shallowReactive只代理第一层属性:

import { shallowReactive, shallowRef, triggerRef } from 'vue';

// shallowReactive:只代理根级属性
const state = shallowReactive({
  list: [],         // 修改list不触发更新(除非替换整个list引用)
  count: 0          // 修改count触发更新
});

state.count = 1;          // 触发更新
state.list.push(1);       // 不触发更新
state.list = [1, 2, 3];   // 触发更新(替换引用)

// shallowRef + triggerRef:手动控制触发时机
const data = shallowRef({ items: [] });
// 批量操作后手动触发
data.value.items.push(...batchData);
triggerRef(data); // 一次性触发更新,避免多次渲染

表格组件渲染10000行数据时,使用shallowRef存储数据 + triggerRef手动触发更新,初始化时间从800ms降至50ms。配合v-memo指令缓存行渲染结果,进一步减少虚拟DOM diff开销:

<tr v-for="item in data.items" :key="item.id" v-memo="[item.id, item.updated]">
  <td>{{ item.name }}</td>
  <td>{{ item.value }}</td>
</tr>

v-memo数组中的依赖未变化时,该行跳过diff直接复用上次渲染结果。数据更新时只有item.updated发生变化的行重新渲染,其余行复用VNode。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-xiang-ying-shi-yuan-li-shen-du-jie-xi-proxy-lan-jie-yu/

(0)
小编小编
上一篇 19小时前
下一篇 19小时前

相关推荐