阅读视图

发现新文章,点击刷新页面。

vue里的ref和reactive的工作原理是什么?以及它们的区别是什么?为什么有人说reactive“废弃”了?

这是一个非常硬核且高质量的问题。要理解 Vue 3 的响应式系统,必须深入到源码层面。

首先,我需要纠正一个重要的误区:Vue 3 并没有“弃用” reactive 。它仍然是 Vue 3 核心 API 的一部分。但是,确实存在一种趋势(包括 Vue 官方团队的建议),推荐优先使用 ref 。

下面我将从源码级别(基于 Vue 3 packages/reactivity 模块)为你详细剖析两者的原理、区别,以及为什么现在大家更倾向于用 ref


一、 reactive 的源码工作原理

reactive 的核心是利用了 ES6 的 Proxy 对象。它拦截对象的操作(增删改查),从而实现依赖收集和触发更新。

1. 核心流程 (reactive.ts)

当你调用 reactive(obj) 时,Vue 内部实际上执行了 createReactiveObject 函数。

简化版源码逻辑:

// 存放代理对象的缓存,防止同一个对象被代理多次
const reactiveMap = new WeakMap();

function createReactiveObject(target) {
  // 1. 如果不是对象(是基础类型),直接返回,无法代理
  if (!isObject(target)) {
    return target;
  }

  // 2. 检查缓存,如果已经代理过,直接返回缓存的 Proxy
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target);
  }

  // 3. 创建 Proxy
  const proxy = new Proxy(target, mutableHandlers);

  // 4. 存入缓存
  reactiveMap.set(target, proxy);
  
  return proxy;
}

2. 拦截器 (baseHandlers.ts)

Proxy 的威力在于第二个参数 mutableHandlers。它定义了 get(读取)和 set(修改)的拦截行为。

  • get (依赖收集) :当副作用函数(Effect,如 computed 或 render)读取属性时,触发 track 函数,将当前 Effect 记录下来。
  • set (派发更新) :当修改属性时,触发 trigger 函数,找到之前收集的 Effect 并执行它们。

简化版 Handler 逻辑:

const mutableHandlers = {
  get(target, key, receiver) {
    // 1. 收集依赖
    track(target, key);
    
    // 2. 获取原本的值
    const res = Reflect.get(target, key, receiver);

    // 3. 【深度响应关键点】如果获取到的 res 是对象,递归将其转为 reactive
    // 这与 Vue 2 不同,Vue 3 是懒代理(访问时才代理),性能更好
    if (isObject(res)) {
      return reactive(res); 
    }
    
    return res;
  },
  
  set(target, key, value, receiver) {
    // 1. 获取旧值
    const oldValue = target[key];
    // 2. 设置新值
    const result = Reflect.set(target, key, value, receiver);
    
    // 3. 如果值发生变化,触发更新
    if (hasChanged(value, oldValue)) {
      trigger(target, key);
    }
    
    return result;
  }
};


二、 ref 的源码工作原理

ref 的设计初衷是为了解决 基本数据类型(Primitives) 无法使用 Proxy 代理的问题(Proxy 只能代理对象)。

1. 核心流程 (ref.ts)

ref 本质上是一个 对象的包装器。它通过定义一个类 RefImpl,利用 ES6 的类属性访问器(getter/setter)来拦截 .value 的访问。

简化版源码逻辑:

function ref(value) {
  return createRef(value);
}

function createRef(rawValue) {
  if (isRef(rawValue)) return rawValue;
  return new RefImpl(rawValue);
}

class RefImpl {
  public _value; // 存储当前值
  public _rawValue; // 存储原始值(用于比较)
  public dep; // 依赖容器
  public __v_isRef = true; // 标记这是一个 Ref

  constructor(value) {
    this._rawValue = value;
    // 核心差异点:如果传入的是对象,内部会自动调用 reactive()!
    this._value = isObject(value) ? reactive(value) : value;
  }

  get value() {
    // 1. 收集依赖
    trackRefValue(this);
    return this._value;
  }

  set value(newVal) {
    // 2. 检查值是否变化
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      // 如果新赋的值是对象,再次转为 reactive
      this._value = isObject(newVal) ? reactive(newVal) : newVal;
      // 3. 触发更新
      triggerRefValue(this);
    }
  }
}

重点总结

  • 如果 ref(10):利用 RefImpl 的 get value 和 set value 进行拦截。
  • 如果 ref({ count: 1 })RefImpl 会将内部的 value 变成 reactive({ count: 1 }) 的 Proxy 对象。

三、 Ref 与 Reactive 的关键区别

特性 Ref Reactive
数据类型 支持所有类型(基本类型 + 对象)。 仅支持对象(Array, Object, Map, Set)。
底层原理 RefImpl 类(getter/setter)。如果是对象,内部转调 reactive 直接使用 Proxy
访问方式 必须通过 .value 访问(模板中自动解包除外)。 直接访问属性。
重新赋值 myRef.value = {} 依然保持响应式。 let state = reactive({}); state = {} 会丢失响应性
结构解构 解构会丢失响应性(需用 toRefs)。 解构会丢失响应性(需用 toRefs)。

四、 为什么说 Vue 3 “想弃用” reactive(实际上是推荐 ref)?

这是一个由 “开发者体验(DX)” 驱动的趋势。虽然 reactive 并没有被官方删除,但社区和尤雨溪(Evan You)都倾向于 “Ref 一把梭” ,主要原因如下:

1. reactive 的局限性会导致 Bug

Vue 新手最常遇到的坑就是 reactive 丢失响应性:

  • 赋值替换问题

    let list = reactive([]);
    // 错误!这样赋值会切断 Proxy 的连接,页面不会更新
    list = [1, 2, 3]; 
    
    // 正确写法(很麻烦)
    list.push(...[1, 2, 3]);
    // 或者再包一层
    const state = reactive({ list: [] });
    state.list = [1, 2, 3];
    

    而 ref 没有这个问题:list.value = [1, 2, 3] 总是安全的。

  • 解构丢失问题

    const state = reactive({ count: 0 });
    let { count } = state; // count 变成了普通数字,不再是响应式的
    

2. 类型不一致带来的心智负担

在一个组件中,如果混用两者:

  • 有些变量直接读(reactive)。
  • 有些变量要加 .value(ref)。
  • 有些对象是通过 props 传进来的,你不知道它是普通对象还是 Proxy。

使用 ref 可以统一心智模型

  • 在 JS 代码中,凡是响应式数据,都要带 .value 。这种显式调用虽然写起来多几个字符,但让代码更清晰,更容易区分哪些是普通变量,哪些是响应式变量。

3. 官方的态度

尤雨溪曾在社区讨论中表示,如果 Vue 3 重新设计,也许会只保留 ref。虽然 reactive 为了 API 的完整性被保留了,但在实际业务开发中,全量使用 ref 是目前最推荐的最佳实践。

总结

  • Reactive: 基于 Proxy,只对对象有效,存在解构和重新赋值丢失响应性的风险。
  • Ref: 基于 class 的 getter/setter,统一了基本类型和对象的处理(对象内部调用 reactive)。
  • 结论: 建议在项目中优先使用 ref 。它更加稳健,不易出错,且能够清晰地标识出“这是一个响应式数据”。

详细说说vue2何vue3的工作原理以及区别?为什么vue3会做此升级?

这是一个非常有分量的架构级问题。要讲清楚 Vue 2 到 Vue 3 的演进,我们需要从底层的响应式原理、虚拟 DOM (Virtual DOM) 的编译策略以及运行时性能三个维度深入剖析。

以下是从源码和架构设计层面的详细解读:


一、 什么是虚拟 DOM (Virtual DOM)?

在理解 Vue 原理之前,必须先理解虚拟 DOM。

1. 定义:
虚拟 DOM 本质上是一个 JavaScript 对象(VNode) ,它是真实 DOM 的“蓝图”或“替身”。

2. 为什么需要它?

  • 操作真实 DOM 代价高昂:真实 DOM 节点非常重(包含大量属性和事件)。频繁操作 DOM 会导致浏览器频繁重排(Reflow)和重绘(Repaint),性能极差。
  • JS 计算很快:在 JS 层面通过对比两个对象(新旧 VNode)的差异(Diff),计算出最小的更变操作,然后再去更新真实 DOM,效率最高。

3. 结构示例:

// 真实 DOM: <div class="box">Hello</div>
// 虚拟 DOM (VNode):
const vnode = {
  tag: 'div',
  props: { class: 'box' },
  children: 'Hello',
  // Vue3 新增了 patchFlag 等编译优化标记
}


二、 Vue 2 的工作原理

Vue 2 的核心是 Options API 和基于 Object.defineProperty 的响应式系统。

1. 响应式原理 (Reactivity)

Vue 2 在初始化(initState)时,会递归遍历 data 中的所有属性。

  • 核心 APIObject.defineProperty

  • 源码逻辑

    • Observer(观察者) :递归把对象属性转为 getter/setter。
    • Dep(依赖容器) :每个属性闭包里都有一个 Dep 实例,用来存放到到底谁用了我。
    • Watcher(订阅者) :组件渲染函数、computed、watch 都是 Watcher。
// Vue 2 响应式简化版
Object.defineProperty(obj, key, {
  get() {
    // 1. 依赖收集:如果当前有正在计算的 Watcher,就把它收集进 Dep
    if (Dep.target) dep.depend();
    return value;
  },
  set(newVal) {
    if (newVal === value) return;
    value = newVal;
    // 2. 派发更新:通知 Dep 里所有的 Watcher 去 update
    dep.notify();
  }
});

2. Vue 2 的痛点

  1. 初始化慢:因为是递归遍历,如果 data 对象很大,启动(Init)阶段会非常耗时,且内存占用高。
  2. 动态性不足:无法监听对象属性的新增(add)和删除(delete),必须用 $set / $delete
  3. 数组限制:无法拦截数组索引修改(arr[0] = 1),Vue 2 重写了数组的 7 个变异方法(push, pop...)来实现响应式。

3. 虚拟 DOM 与 Diff (Vue 2)

Vue 2 的 Diff 算法是 全量对比
当数据变化时,Vue 2 会重新生成整个组件的 VNode 树,然后和旧的 VNode 树进行对比(双端比较算法)。即使有些节点及其子节点永远不会变(静态节点),Vue 2 依然会去比对它们。


三、 Vue 3 的工作原理

Vue 3 在响应式系统和编译优化上做了彻底的重构。

1. 响应式原理 (Reactivity)

Vue 3 使用 Proxy 替代了 defineProperty。代码位于 packages/reactivity

  • 核心 APIProxy + Reflect

  • 源码逻辑

    • 不再需要 Observer 类,直接返回一个 Proxy 代理。
    • Track(依赖收集) :当读取属性时触发 track(target, key),将副作用函数(Effect)存入全局的 WeakMap
    • Trigger(派发更新) :当修改属性时触发 trigger(target, key),从 WeakMap 取出 Effect 执行。
// Vue 3 响应式简化版
new Proxy(target, {
  get(target, key, receiver) {
    track(target, key); // 收集依赖
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver);
    trigger(target, key); // 触发更新
    return res;
  }
})

  • 优势

    • 懒代理(Lazy) :只有访问到深层对象时,才会将其转为 Proxy,初始化飞快。
    • 全能拦截:支持新增、删除属性,支持数组索引修改,支持 Map/Set。

2. 编译优化 (Compiler Optimization) —— 核心升级

Vue 3 的 Diff 算法不仅仅是快,而是**“更聪明”** 。它在编译阶段(Template -> Render Function) 做了大量标记,让运行时(Runtime) 跑得更快。

  • PatchFlags (动态标记) :
    在编译时,Vue 3 会分析模板,给动态节点打上“二进制标记”。
    比如:<div :class="cls">123</div>
    Vue 3 知道只有 class 是动态的,Diff 时只对比 class ,完全忽略内容。

  • Block Tree (区块树) :
    Vue 3 将模板切分成 Block,配合 PatchFlags,Diff 时直接跳过静态节点,只遍历动态节点数组。

    • Vue 2 Diff 复杂度 = 模板总体积
    • Vue 3 Diff 复杂度 = 动态节点的数量
  • Hoist Static (静态提升) :
    静态的节点(如 <p>永远不变</p>)在内存中只创建一次,后续更新直接复用,不再重复创建 VNode。


四、 Vue 2 和 Vue 3 的比较与区别

特性 Vue 2 Vue 3
响应式底层 Object.defineProperty Proxy
检测能力 无法检测属性增删、数组索引修改 完全支持
初始化性能 递归遍历所有属性(慢、内存高) 懒代理,访问时才转换(快)
代码组织 Options API (data, methods 分离) Composition API (逻辑关注点聚合)
Diff 算法 全量双端比较,静态节点也要比 静态标记 + Block Tree,只比动态节点
TypeScript 支持较弱,类型推断困难 核心由 TS 编写,TS 支持极其友好
体积 较大,难以 Tree-shaking 模块化拆分,支持 Tree-shaking,体积更小
Fragment 组件只能有一个根节点 支持多根节点 (Fragment)

五、 为什么 Vue 3 要做这些升级?

尤雨溪和团队进行 Vue 3 重构主要为了解决 Vue 2 的三个核心瓶颈:

1. 性能瓶颈 (Performance)

Vue 2 的响应式初始化是递归的,对于大数据量的表格或列表,启动非常慢。且 Diff 算法在大型复杂组件中,无谓的静态节点对比消耗了大量 CPU。
Vue 3 通过 Proxy 和编译优化(静态标记),实现了“按需响应”和“靶向更新”,性能大幅提升。

2. 代码组织与复用瓶颈 (Scalability)

在 Vue 2 的 Options API 中,一个功能的逻辑被拆分到 datamethodswatch 里。当组件变得巨大(几千行代码)时,维护代码需要在文件里上下反复横跳(Jumping)。且 Mixin 代码复用存在命名冲突和来源不清晰的问题。
Vue 3 引入 Composition API (组合式 API) ,允许开发者按“逻辑功能”组织代码,完美解决了大型项目的维护难题,Hooks 更是取代了 Mixin。

3. TypeScript 支持 (Developer Experience)

Vue 2 的源码是 JS 写的,通过 Flow 做类型检查,对 TS 的支持是后期补丁(this 指向在 TS 中很难推断)。随着前端工程化对 TS 需求的爆发,Vue 2 显得力不从心。
Vue 3 使用 TypeScript 重写,提供了原生的、极佳的类型推断体验。

总结

  • 原理层面:Vue 2 是劫持 setter/getter,Vue 3 是代理整个对象。
  • 更新机制:Vue 2 是全量树对比,Vue 3 是基于静态标记的动态节点追踪。
  • 目的:Vue 3 的升级是为了更快(性能)、更小(体积)、更易维护(组合式 API)以及更好的 TS 支持

❌