Vue 响应式原理

Vue 的响应式原理是其核心特性之一,核心目标是实现「数据驱动视图」------ 当数据发生变化时,依赖该数据的视图会自动更新,无需手动操作 DOM。这一机制在 Vue 2 和 Vue 3 中实现方式有较大差异,以下分别详细说明:

Vue 2 的响应式原理(基于 Object.defineProperty

Vue 2 采用 Object.defineProperty 对数据进行劫持,结合「发布 - 订阅模式」实现响应式,核心流程包括数据劫持依赖收集触发更新三个环节。

Vue 2 的响应式系统核心是通过「数据劫持」+「依赖收集」实现数据变化到视图更新的自动触发,整体流程可分为初始化、依赖收集、触发更新三个阶段。

初始化阶段: Vue 会对组件的 data 进行处理。(1)通过 Object.defineProperty 为每个属性添加 getset 拦截器,将普通对象转为响应式对象;(2)同时为每个属性创建对应的 Dep 实例(依赖管理器),用于存储依赖该属性的 Watcher

依赖收集阶段: 发生在数据首次被读取时。当组件初始化或 watch/计算属性触发时,会先实例化 Watcher(如渲染 Watcher 负责视图更新、watchWatcher 对应回调逻辑),并执行 Watcherget 方法------此时会将 Dep.target 设为当前 Watcher,再执行 getter 函数(如渲染函数读取 data 属性)。读取属性时触发 get 拦截器,Dep 会通过 Dep.target 识别当前活跃的 Watcher,将其添加到依赖列表中,完成"数据-Watcher"的关联。

触发更新阶段: 发生在数据被修改时。当属性值变化,会触发 set 拦截器,此时对应的 Dep 会遍历依赖列表中的所有 Watcher,调用其 update 方法;Watcher 会通过异步队列(避免频繁更新)触发最终操作,如渲染 Watcher 重新执行渲染函数更新视图,watchWatcher 执行用户定义的回调,从而实现数据变化后视图或逻辑的自动响应。

数据劫持:拦截数据的读写

Object.defineProperty 是 ES5 提供的 API,通过定义对象属性的 get(读取时触发)和 set(修改时触发)方法,实现对属性的拦截。Vue 2 会对 data 中的数据递归执行这一操作,使其成为「响应式数据」。

scss 复制代码
// 递归将对象转为响应式
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return; // 非对象类型无需处理
  }
  // 遍历对象属性,逐个劫持
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

// 劫持单个属性
function defineReactive(obj, key, val) {
  // 递归处理子对象(如 data 中的嵌套对象)
  observe(val);

  // 依赖管理器:收集依赖当前属性的订阅者
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    get() {
      // 读取属性时,收集依赖(当前活跃的 Watcher)
      if (Dep.target) {
        dep.addSub(Dep.target); // 将 Watcher 加入依赖列表
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return; // 值未变化则不处理
      val = newVal;
      observe(newVal); // 新值如果是对象,需要递归劫持
      // 修改属性时,通知所有依赖更新
      dep.notify();
    }
  });
}

只要访问了响应式对象的属性,就会触发 get() 拦截器 ,无论这种访问是来自模板渲染、watchcomputed,还是手动代码。get() 的核心作用是在属性被访问时,记录 "谁在访问它"(即 Dep.target 指向的 Watcher ,从而建立 "数据→依赖" 的关联,为后续数据变化时的更新通知打下基础。

依赖收集:跟踪使用数据的地方

当响应式数据被读取时(如组件渲染、watch 监听),Vue 需要记录「谁在使用这个数据」(即「依赖」),这一过程称为依赖收集。核心通过 Dep(依赖管理器)和 Watcher(订阅者)实现:

Dep :每个响应式属性对应一个 Dep 实例,用于存储依赖该属性的所有 Watcher

javascript 复制代码
class Dep {
  static target = null; // 静态属性,指向当前活跃的 Watcher
  subs = []; // 存储订阅者(Watcher)

  // 添加订阅者
  addSub(sub) {
    this.subs.push(sub);
  }

  // 通知所有订阅者更新
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

Watcher :组件的渲染逻辑、watch 选项、computed 属性等都会被包装成 Watcher。当依赖的数据变化时,Watcher 会触发更新(如重新渲染组件)。

kotlin 复制代码
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm; // 当前组件实例
    this.cb = cb; // 更新时执行的回调(如重新渲染)
    this.getter = expOrFn; // 依赖的表达式或渲染函数
    this.get(); // 初始化时触发 get,收集依赖
  }

  get() {
    Dep.target = this; // 标记当前活跃的 Watcher
    this.getter.call(this.vm); // 执行渲染函数,触发数据的 get 拦截
    Dep.target = null; // 重置,避免重复收集
  }

  // 数据变化时触发更新
  update() {
    this.cb(); // 如重新执行渲染函数
  }
}

依赖收集过程 :当组件首次渲染时,会执行渲染函数,过程中会读取 data 中的属性,触发 get 拦截器。此时 Dep.target 指向当前组件的 WatcherDep 会将该 Watcher 加入订阅列表,完成依赖收集。

触发更新:数据变化时通知依赖

当修改响应式数据时,会触发 set 拦截器,Dep 会调用 notify() 方法,通知所有订阅的 Watcher 执行 update(),最终触发组件重新渲染或 watch 回调,实现「数据变 → 视图变」。

Vue 2 响应式的局限性

由于 Object.defineProperty 的设计限制,Vue 2 存在以下问题:

  • 无法监听对象新增 / 删除的属性 :只能劫持初始化时已存在的属性,新增属性需通过 this.$set(obj, key, val) 手动触发响应式。
  • 无法监听数组的部分操作 :数组的索引修改(如 arr[0] = 1)、length 变化不会触发 set,因此 Vue 2 重写了数组的 7 个方法(pushpopsplice 等)以支持响应式。
  • 深层对象递归劫持的性能问题:初始化时需递归劫持所有嵌套对象,数据结构复杂时可能影响性能。

Vue3

Vue 3 的响应式系统基于 Proxy 代理Effect 副作用机制实现,核心是建立"数据变化-副作用执行"的自动关联,流程可分为初始化、依赖收集、触发更新三个阶段。

初始化阶段: 通过 reactiveref 将数据转为响应式对象:reactive 针对对象/数组,使用 Proxy 代理整个对象,拦截 get(读取)、set(修改)、deleteProperty(删除)等操作;ref 针对基本类型,通过封装成带 value 属性的对象,内部同样用 Proxy 代理 value 的读写。同时,每个响应式对象的属性会对应关联的"依赖容器",用于存储依赖该属性的副作用。

依赖收集阶段: 发生在副作用函数执行时。当通过 effect 注册副作用(如组件渲染函数、watch 回调),effect 会先将当前副作用标记为"活跃状态",再执行副作用函数。函数执行中若读取响应式属性,会触发 Proxy 的 get 拦截器:拦截器会定位该属性对应的依赖容器,将活跃副作用添加到容器中,完成"数据-副作用"的关联。

触发更新阶段: 发生在数据被修改时。当修改响应式属性(如赋值、删除),会触发 Proxy 的 setdeleteProperty 拦截器:拦截器会先更新数据,再取出该属性依赖容器中的所有副作用,通过调度器(如控制执行时机、避免重复执行)触发副作用重新执行,从而实现数据变化后视图或逻辑的自动响应。 相比 Vue 2 的 Object.defineProperty,Proxy 能原生支持对象新增属性、数组索引/方法修改等场景,无需额外处理,响应式覆盖更全面。

cn.vuejs.org/guide/extra...

数据劫持:Proxy 拦截整个对象

Proxy 可以创建一个对象的代理,直接拦截对象的读取、修改、新增、删除等操作,无需逐个拦截属性,支持更全面的响应式劫持。

javascript 复制代码
// 创建响应式对象
function reactive(target) {
  return new Proxy(target, {
    // 读取属性时触发(包括对象属性、数组索引、length 等)
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver); // 反射读取,确保正确的 this 指向
      // 收集依赖
      track(target, key);
      // 如果属性值是对象,递归创建代理(懒递归:访问时才处理,优化性能)
      if (typeof res === 'object' && res !== null) {
        return reactive(res);
      }
      return res;
    },

    // 修改/新增属性时触发
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      if (oldValue !== value) {
        Reflect.set(target, key, value, receiver);
        // 触发更新
        trigger(target, key);
      }
      return true;
    },

    // 删除属性时触发
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) {
        // 触发更新
        trigger(target, key);
      }
      return result;
    }
  });
}

依赖收集与触发:tracktrigger

Vue 3 用 ReactiveEffect(替代 Vue 2 的 Watcher)管理依赖,核心逻辑更简洁。


track (收集依赖) :当读取响应式数据时,记录当前的 ReactiveEffect 与数据的关联。

ini 复制代码
// 存储依赖:target → key → 依赖集合
const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) return; // 没有活跃的 effect 则不收集
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(activeEffect); // 将当前活跃的 effect 加入依赖
}

trigger (触发更新) :当数据变化时,找到所有关联的 ReactiveEffect 并执行其更新逻辑。

ini 复制代码
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect.run()); // 执行所有依赖的更新
  }
}

ReactiveEffect :封装副作用函数(如渲染函数),当依赖变化时重新执行。

javascript 复制代码
let activeEffect = null;

class ReactiveEffect {
  constructor(fn) {
    this.fn = fn; // 副作用函数(如组件渲染函数)
  }

  run() {
    activeEffect = this; // 标记当前活跃的 effect
    this.fn(); // 执行副作用函数(触发数据读取,进而收集依赖)
    activeEffect = null; // 重置
  }
}

对基本类型的支持:ref

Proxy 只能代理对象,对于基本类型(如 numberstring),Vue 3 提供 ref 包装。使用时通过 .value 访问,在模板中会自动解包(无需显式写 .value)。

scss 复制代码
// 模拟 Vue 内部的 ref 函数
function ref(value) {
  // 如果是对象,先转为 reactive(Proxy 代理)
  if (isObject(value)) {
    value = reactive(value);
  }

  // 创建 Ref 对象(通过 getter/setter 劫持 value 属性)
  const refObject = {
    get value() {
      track(refObject, 'value'); // 收集依赖
      return value;
    },
    set value(newValue) {
      // 如果新值是对象,也需要转为 reactive
      if (isObject(newValue)) {
        newValue = reactive(newValue);
      }
      value = newValue;
      trigger(refObject, 'value'); // 触发更新
    }
  };

  return refObject;
}

Vue 3 响应式的优势

  • 支持对象新增 / 删除属性Proxy 能拦截 set(新增)和 deleteProperty(删除)操作,无需手动调用 $set
  • 原生支持数组响应式 :可监听数组索引修改、length 变化等,无需重写数组方法。
  • 懒递归劫持:嵌套对象只有在被访问时才会创建代理,初始化性能更优。
  • 支持复杂数据结构 :如 MapSet 等,Proxy 可拦截其 setdelete 等操作。
相关推荐
anyup13 分钟前
🔥🔥 10 天 Star 破百!uView Pro 文档也开源啦:完全免费、无广告、高效上手
前端·vue.js·uni-app
南半球与北海道#1 小时前
el-table合并单元格
javascript·vue.js·elementui·表格合并
啷咯哩咯啷2 小时前
element-plus el-tree-v2大数据量勾选节点卡顿问题
前端·javascript·vue.js
焦小风Zephyr3 小时前
Vue3组件通信:父子相传
vue.js
Sky_Ax3 小时前
实现一个时间轴组件
vue.js
RainbowSea4 小时前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 05
vue.js·spring boot·后端
文艺理科生5 小时前
Nuxt 组件渲染进阶:服务端与客户端组件的协作艺术
前端·javascript·vue.js
天客5 小时前
Vue3.5 + Element-Plus 二次封装 el-table、el-form、el-descriptions
前端·vue.js
XiaoMu_0016 小时前
【Vue vs React:前端框架深度对比分析】
vue.js·react.js·前端框架