🔍 重写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应用非常重要。

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

相关推荐
摇滚侠1 天前
Spring Boot 3零基础教程,WEB 开发 默认的自动配置,笔记25
前端·spring boot·笔记
Cherry Zack1 天前
Vue Router 路由管理完全指南:从入门到精通前言
前端·javascript·vue.js
亮子AI1 天前
【npm】npm install 产生软件包冲突怎么办?(详细步骤)
前端·npm·node.js
汪汪大队u1 天前
为什么 filter-policy 仅对 ASBR 的出方向生效,且即使在该生效场景下,被过滤的路由在协议内部(如协议数据库)依然存在,没有被彻底移除?
服务器·前端·网络
慧一居士1 天前
vue.config.js 文件功能介绍,使用说明,对应完整示例演示
前端·vue.js
颜酱1 天前
用导游的例子来理解 Visitor 模式,实现AST 转换
前端·javascript·算法
木易 士心1 天前
Nginx 基本使用和高级用法详解
运维·javascript·nginx
蒙特卡洛的随机游走1 天前
Spark的宽依赖与窄依赖
大数据·前端·spark
共享家95271 天前
QT-常用控件(多元素控件)
开发语言·前端·qt
幸运小圣1 天前
Iterator迭代器 【ES6】
开发语言·javascript·es6