🔍 重写vue之ref和reactive

🎯 学习目标:深入理解Vue3响应式系统的底层实现原理,掌握ref和reactive的源码机制

📊 难度等级 :中级-高级

🏷️ 技术标签#Vue3 #响应式原理 #源码解析 #Proxy #WeakMap

⏱️ 阅读时间:约15分钟


🌟 引言

在Vue3开发中,你是否遇到过这样的困扰:

  • 响应式失效:明明修改了数据,但视图就是不更新
  • 性能问题:不知道为什么某些操作会触发大量的重新渲染
  • 类型困惑:ref和reactive到底有什么区别,什么时候用哪个
  • 调试困难:响应式数据的变化追踪不清楚,调试时一头雾水

今天我们从源码层面深入解析Vue3响应式系统的5个核心机制,让你彻底理解ref和reactive的实现原理,写出更高效的Vue3代码!


💡 核心机制详解

1. Proxy代理机制:响应式的基石

🔍 应用场景

Vue3使用Proxy替代Vue2的Object.defineProperty,实现更强大的响应式系统

❌ Vue2的局限性

javascript 复制代码
// ❌ Vue2 Object.defineProperty的问题
const data = { name: 'Vue2' };
Object.defineProperty(data, 'name', {
  get() {
    console.log('访问name');
    return this._name;
  },
  set(value) {
    console.log('设置name');
    this._name = value;
  }
});

// 无法监听新增属性
data.age = 18; // 不会触发响应式
// 无法监听数组索引变化
const arr = [1, 2, 3];
arr[0] = 10; // 不会触发响应式

✅ Vue3的Proxy方案

javascript 复制代码
/**
 * 创建响应式代理对象
 * @description 使用Proxy拦截对象的所有操作
 * @param {Object} target - 目标对象
 * @param {Object} handler - 代理处理器
 * @returns {Proxy} 代理对象
 */
const createReactiveProxy = (target, handler) => {
  return new Proxy(target, handler);
};

// Vue3响应式处理器
const reactiveHandler = {
  get(target, key, receiver) {
    // 依赖收集
    track(target, key);
    const result = Reflect.get(target, key, receiver);
    
    // 深度响应式处理
    if (isObject(result)) {
      return reactive(result);
    }
    
    return result;
  },
  
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    
    // 触发更新
    if (oldValue !== value) {
      trigger(target, key, value, oldValue);
    }
    
    return result;
  },
  
  has(target, key) {
    track(target, key);
    return Reflect.has(target, key);
  },
  
  deleteProperty(target, key) {
    const hadKey = hasOwn(target, key);
    const result = Reflect.deleteProperty(target, key);
    
    if (result && hadKey) {
      trigger(target, key, undefined);
    }
    
    return result;
  }
};

💡 核心要点

  • 全面拦截:Proxy可以拦截对象的所有操作(get、set、has、delete等)
  • 动态属性:支持新增属性的响应式监听
  • 数组支持:完美支持数组索引和length的变化监听
  • 性能优化:只有在访问时才进行深度响应式转换

🎯 实际应用

javascript 复制代码
// 实际项目中的应用
const state = reactive({
  user: { name: 'Vue3', age: 3 },
  list: [1, 2, 3]
});

// 所有操作都会被监听
state.user.name = 'Vue3.0'; // ✅ 响应式
state.user.email = 'vue@vue.js'; // ✅ 响应式
state.list.push(4); // ✅ 响应式
state.list[0] = 10; // ✅ 响应式
delete state.user.age; // ✅ 响应式

2. 依赖收集系统:track函数的实现

🔍 应用场景

当组件访问响应式数据时,需要建立数据与组件的依赖关系

❌ 简单的观察者模式

javascript 复制代码
// ❌ 简单但不够精确的依赖收集
let currentEffect = null;
const deps = new Set();

function track() {
  if (currentEffect) {
    deps.add(currentEffect);
  }
}

function trigger() {
  deps.forEach(effect => effect());
}

✅ Vue3的精确依赖收集

javascript 复制代码
/**
 * 全局依赖收集映射
 * @description WeakMap结构:target -> Map(key -> Set(effects))
 */
const targetMap = new WeakMap();
let activeEffect = null;
const effectStack = [];

/**
 * 依赖收集函数
 * @description 收集当前活跃的effect与响应式数据的依赖关系
 * @param {Object} target - 目标对象
 * @param {string|symbol} key - 属性键
 */
const track = (target, key) => {
  // 没有活跃的effect,不需要收集依赖
  if (!activeEffect) return;
  
  // 获取target对应的依赖映射
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  
  // 获取key对应的effect集合
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  
  // 建立双向依赖关系
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
};

/**
 * 创建响应式effect
 * @description 包装函数,使其能够被依赖收集系统追踪
 * @param {Function} fn - 要执行的函数
 * @param {Object} options - 配置选项
 * @returns {Function} 响应式effect函数
 */
const effect = (fn, options = {}) => {
  const effectFn = () => {
    // 清理旧的依赖关系
    cleanup(effectFn);
    
    try {
      // 设置当前活跃的effect
      effectStack.push(effectFn);
      activeEffect = effectFn;
      
      // 执行函数,触发依赖收集
      return fn();
    } finally {
      // 恢复上一个effect
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  };
  
  effectFn.deps = [];
  effectFn.options = options;
  
  // 立即执行一次
  if (!options.lazy) {
    effectFn();
  }
  
  return effectFn;
};

💡 核心要点

  • WeakMap结构:target -> Map(key -> Set(effects)),避免内存泄漏
  • effect栈:支持effect嵌套执行
  • 双向依赖:effect记录自己依赖的数据,数据记录依赖自己的effect
  • 精确收集:只收集真正被访问的属性的依赖

🎯 实际应用

javascript 复制代码
// 实际项目中的依赖收集
const state = reactive({ count: 0, name: 'Vue' });

// 创建计算属性
const doubleCount = computed(() => {
  return state.count * 2; // 只依赖count,不依赖name
});

// 创建监听器
watchEffect(() => {
  console.log(`Count: ${state.count}`); // 只依赖count
});

// 只有修改count才会触发更新
state.count++; // ✅ 触发doubleCount和watchEffect
state.name = 'Vue3'; // ❌ 不会触发任何更新

3. 触发更新机制:trigger函数的实现

🔍 应用场景

当响应式数据发生变化时,需要通知所有依赖该数据的effect执行更新

❌ 简单的全量更新

javascript 复制代码
// ❌ 简单但性能差的触发机制
function trigger() {
  // 触发所有effect,无论是否相关
  allEffects.forEach(effect => effect());
}

✅ Vue3的精确触发机制

javascript 复制代码
/**
 * 触发更新函数
 * @description 精确触发与特定属性相关的effect
 * @param {Object} target - 目标对象
 * @param {string} type - 操作类型(SET, ADD, DELETE等)
 * @param {string|symbol} key - 属性键
 * @param {any} newValue - 新值
 * @param {any} oldValue - 旧值
 */
const trigger = (target, type, key, newValue, oldValue) => {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const effects = new Set();
  const computedRunners = new Set();
  
  // 收集需要执行的effect
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect) {
          if (effect.options.computed) {
            computedRunners.add(effect);
          } else {
            effects.add(effect);
          }
        }
      });
    }
  };
  
  // 处理不同类型的操作
  if (type === 'clear') {
    // 清空操作,触发所有effect
    depsMap.forEach(add);
  } else if (key === 'length' && isArray(target)) {
    // 数组length变化的特殊处理
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newValue) {
        add(dep);
      }
    });
  } else {
    // 普通属性变化
    if (key !== void 0) {
      add(depsMap.get(key));
    }
    
    // 新增属性的特殊处理
    if (type === 'add') {
      if (!isArray(target)) {
        add(depsMap.get(ITERATE_KEY));
      } else if (isIntegerKey(key)) {
        add(depsMap.get('length'));
      }
    }
  }
  
  // 执行effect,computed优先
  const run = (effect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect);
    } else {
      effect();
    }
  };
  
  // 先执行computed
  computedRunners.forEach(run);
  // 再执行普通effect
  effects.forEach(run);
};

💡 核心要点

  • 精确触发:只触发真正依赖变化属性的effect
  • 类型区分:根据操作类型(SET、ADD、DELETE)采用不同策略
  • 优先级控制:computed effect优先于普通effect执行
  • 调度器支持:支持自定义effect执行时机

🎯 实际应用

javascript 复制代码
// 实际项目中的精确触发
const state = reactive({
  user: { name: 'Vue', age: 3 },
  list: [1, 2, 3],
  count: 0
});

// 不同的effect监听不同的属性
effect(() => console.log('user name:', state.user.name)); // effect1
effect(() => console.log('list length:', state.list.length)); // effect2
effect(() => console.log('count:', state.count)); // effect3

// 精确触发
state.user.name = 'Vue3'; // 只触发effect1
state.list.push(4); // 只触发effect2
state.count++; // 只触发effect3

4. ref的实现机制:值类型的响应式包装

🔍 应用场景

为基本类型数据(string、number、boolean等)提供响应式能力

❌ 直接使用基本类型

javascript 复制代码
// ❌ 基本类型无法直接响应式
let count = 0;
// 无法监听count的变化
count++; // 不会触发任何更新

✅ ref的响应式包装

javascript 复制代码
/**
 * ref实现类
 * @description 为基本类型提供响应式包装
 */
class RefImpl {
  constructor(value, shallow = false) {
    this._shallow = shallow;
    this._rawValue = shallow ? value : toRaw(value);
    this._value = shallow ? value : convert(value);
  }
  
  get value() {
    // 依赖收集
    track(this, 'value');
    return this._value;
  }
  
  set value(newValue) {
    // 比较新旧值
    newValue = this._shallow ? newValue : toRaw(newValue);
    
    if (hasChanged(newValue, this._rawValue)) {
      this._rawValue = newValue;
      this._value = this._shallow ? newValue : convert(newValue);
      
      // 触发更新
      trigger(this, 'set', 'value', newValue);
    }
  }
}

/**
 * 创建ref响应式引用
 * @description 将基本类型包装为响应式对象
 * @param {any} value - 初始值
 * @returns {RefImpl} ref对象
 */
const ref = (value) => {
  return createRef(value, false);
};

/**
 * 创建浅层ref
 * @description 只对.value的赋值响应,不深度转换对象
 * @param {any} value - 初始值
 * @returns {RefImpl} 浅层ref对象
 */
const shallowRef = (value) => {
  return createRef(value, true);
};

/**
 * 创建ref的内部实现
 * @description ref和shallowRef的统一创建函数
 * @param {any} rawValue - 原始值
 * @param {boolean} shallow - 是否浅层
 * @returns {RefImpl} ref实例
 */
const createRef = (rawValue, shallow) => {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
};

/**
 * 值转换函数
 * @description 将值转换为响应式(如果是对象)
 * @param {any} value - 要转换的值
 * @returns {any} 转换后的值
 */
const convert = (value) => {
  return isObject(value) ? reactive(value) : value;
};

💡 核心要点

  • 值包装:通过.value属性访问和修改实际值
  • 类型转换:对象类型自动转换为reactive
  • 浅层模式:shallowRef只监听.value的变化
  • 自动解包:在模板中自动解包,无需.value

🎯 实际应用

javascript 复制代码
// 实际项目中的ref使用
const count = ref(0);
const user = ref({ name: 'Vue', age: 3 });

// 在setup中需要.value
const increment = () => {
  count.value++; // 触发响应式更新
};

const updateUser = () => {
  user.value.name = 'Vue3'; // 深度响应式
  // 或者
  user.value = { name: 'Vue3', age: 4 }; // 替换整个对象
};

// 在模板中自动解包
// <template>
//   <div>{{ count }}</div> <!-- 无需.value -->
//   <div>{{ user.name }}</div> <!-- 无需.value -->
// </template>

5. reactive与ref的区别与选择

🔍 应用场景

理解两者的差异,在合适的场景选择合适的API

❌ 错误的使用方式

javascript 复制代码
// ❌ 错误的使用方式
const state1 = reactive(0); // 基本类型不能用reactive
const state2 = ref({ a: 1, b: 2 }); // 对象用ref需要.value访问

// 解构丢失响应式
const { count } = reactive({ count: 0 });
count++; // 不会触发更新

✅ 正确的使用方式

javascript 复制代码
/**
 * reactive和ref的正确使用场景
 * @description 根据数据类型选择合适的响应式API
 */

// ✅ reactive:用于对象类型
const state = reactive({
  count: 0,
  user: { name: 'Vue', age: 3 },
  list: [1, 2, 3]
});

// ✅ ref:用于基本类型
const count = ref(0);
const name = ref('Vue');
const isLoading = ref(false);

// ✅ ref:用于需要整体替换的对象
const userInfo = ref({ name: 'Vue', age: 3 });
userInfo.value = { name: 'Vue3', age: 4 }; // 整体替换

// ✅ toRefs:解构reactive对象保持响应式
const { count: reactiveCount, user } = toRefs(state);
reactiveCount.value++; // 保持响应式

/**
 * 响应式数据组合函数
 * @description 展示在组合式API中的最佳实践
 * @returns {Object} 响应式数据和方法
 */
const useCounter = () => {
  const count = ref(0);
  const doubleCount = computed(() => count.value * 2);
  
  const increment = () => {
    count.value++;
  };
  
  const decrement = () => {
    count.value--;
  };
  
  return {
    count: readonly(count), // 只读暴露
    doubleCount,
    increment,
    decrement
  };
};

/**
 * 复杂状态管理
 * @description 结合reactive和ref的复杂场景
 * @returns {Object} 状态管理对象
 */
const useUserStore = () => {
  // 用reactive管理复杂对象
  const state = reactive({
    users: [],
    currentUser: null,
    filters: {
      keyword: '',
      status: 'all'
    }
  });
  
  // 用ref管理简单状态
  const loading = ref(false);
  const error = ref(null);
  
  const fetchUsers = async () => {
    loading.value = true;
    error.value = null;
    
    try {
      const users = await api.getUsers(state.filters);
      state.users = users;
    } catch (err) {
      error.value = err.message;
    } finally {
      loading.value = false;
    }
  };
  
  return {
    ...toRefs(state), // 解构reactive
    loading,
    error,
    fetchUsers
  };
};

💡 核心要点

  • reactive:用于对象、数组等引用类型,直接访问属性
  • ref:用于基本类型或需要整体替换的对象,通过.value访问
  • toRefs:将reactive对象转换为ref对象,保持响应式
  • 性能考虑:reactive性能更好,ref有额外的.value包装开销

🎯 实际应用

javascript 复制代码
// 实际项目中的最佳实践
export default {
  setup() {
    // 简单值用ref
    const count = ref(0);
    const message = ref('Hello');
    
    // 复杂对象用reactive
    const form = reactive({
      username: '',
      password: '',
      remember: false
    });
    
    // 需要整体替换的对象用ref
    const userProfile = ref(null);
    
    const login = async () => {
      const user = await api.login(form);
      userProfile.value = user; // 整体替换
    };
    
    return {
      count,
      message,
      ...toRefs(form), // 解构保持响应式
      userProfile,
      login
    };
  }
};

📊 机制对比总结

机制 核心作用 关键技术 性能特点
Proxy代理 拦截对象操作 Proxy + Reflect 按需代理,性能优秀
依赖收集 建立数据-视图关系 WeakMap + Set 精确收集,避免内存泄漏
触发更新 通知相关组件更新 调度器 + 优先级 精确触发,减少无效更新
ref包装 基本类型响应式 .value访问器 轻量包装,自动解包
API选择 不同场景的最佳实践 reactive/ref 按需选择,性能最优

🎯 实战应用建议

最佳实践

  1. Proxy机制:充分利用Proxy的强大能力,支持动态属性和数组操作
  2. 依赖收集:理解WeakMap结构,避免手动管理依赖关系
  3. 触发更新:利用精确触发机制,优化组件更新性能
  4. ref使用:基本类型用ref,复杂对象用reactive
  5. API组合:合理使用toRefs、computed等API组合

性能考虑

  • 按需代理:只有访问时才进行深度响应式转换
  • 精确更新:只触发真正依赖变化数据的组件
  • 内存管理:WeakMap自动垃圾回收,避免内存泄漏
  • 调度优化:computed优先执行,减少重复计算

💡 总结

这5个Vue3响应式核心机制构成了强大的响应式系统:

  1. Proxy代理机制:提供全面的对象操作拦截能力
  2. 依赖收集系统:建立精确的数据-视图依赖关系
  3. 触发更新机制:实现高效的组件更新调度
  4. ref包装机制:为基本类型提供响应式能力
  5. API选择策略:在不同场景选择最优的响应式方案

理解这些底层机制,能帮助你写出更高效、更可维护的Vue3代码,避免常见的响应式陷阱!


🔗 相关资源


💡 今日收获:深入理解了Vue3响应式系统的5个核心机制,这些知识点对于编写高性能Vue3应用非常重要。

如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀

相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu8 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss