普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月17日首页

Vue的响应式系统是怎么实现的

作者 程序员ys
2025年11月16日 22:31

Vue的响应式系统是其核心特性之一,通过数据劫持完成依赖收集和触发更新,实现了数据变化时自动更新视图的功能。

我们自然地联想到观察者模式:当一个对象的状态发生改变时,所有依赖它的对象都得到通知并自动更新

其中,数据是被观察者(subject),视图是观察者(observer),视图对象被注册到它依赖的数据对象,当数据变更时会调用视图对象的更新函数。

一、Vue 2 的响应式系统(基于  Object.defineProperty )

Vue2的响应式系统是基于 Object.defineProperty 实现的,通过修改对象属性行为,劫持对data对象属性的读写操作,完成依赖收集和触发更新,从而实现响应式的功能。

1.数据劫持

Vue 2 使用Object.defineProperty修改对象属性行为,劫持data对象属性的读写操作:

  • Getter:当访问data对象属性时,收集依赖该属性的视图对象。
  • Setter:当修改data对象属性值时,通知依赖该属性的视图对象重新渲染。

2.依赖收集

  • 每个data属性维护一个依赖列表,存放依赖该属性的视图对象。
  • 当data属性被读取时(组件渲染、watch),触发Getter。
  • Getter将当前的视图对象添加到依赖列表中。

3.触发更新

  • 当属性值被修改时,Setter会通知依赖该属性的视图重新渲染。

4.核心代码

// 修改对象属性行为,劫持data对象属性的读写操作
function observe(obj) { 
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
});

function defineReactive(obj, key, val) {
  const dep = new Dep(); // 依赖管理器
  
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集
      if (Dep.target) {
        dep.addSub(Dep.target);
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 触发更新
      dep.notify();
    }
  });
}
// 依赖管理器
class Dep {
  constructor() {
    this.subs = [];
  }
  
  addSub(sub) {
    this.subs.push(sub);
  }
  
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

Dep.target = null; // 当前正在渲染的Watcher

// 视图对象
class Watcher {
  constructor(vm, expOrFn,cb) {
    this.vm = vm;
    this.getter = expOrFn;  // 渲染函数
    this.cb = cb;
    this.value = this.get();
  }
  
  get() {
    Dep.target = this;
    const value = this.getter.call(this.vm);
    Dep.target = null;
    return value;
  }
  
  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

5. Object.defineProperty的不足

Object.defineProperty只能劫持对象属性,无法劫持整个对象,这是其不足之处的根本原因。

  • 动态添加的属性不具有响应式

Object.defineProperty只能劫持现有的对象属性,这意味在初始化data对象时,如果某个属性不存在,后续动态添加的属性将不具有响应式功能。

const vm = new Vue({ data: { obj: { a: 1 } } });
vm.obj.b = 2; // ❌ 新增属性b,非响应式
delete vm.obj.a; // ❌ 删除属性a,非响应式

// 必须使用 Vue.set/Vue.delete
Vue.set(vm.user, 'gender', 'male')
Vue.delete(vm.user, 'age')

在动态表单场景中,新增或删除表单字段时,无法实现响应式。

  • 无法监听数组索引赋值和数组长度的变化
vm.items[0] = 'hi'; // ❌ 索引赋值,非响应式
vm.items.length = 0; // ❌ 修改数组长度,非响应式

// 必须使用vue方法或 Vue.set
vm.list.splice(0, 1, 999)  // 正确方式
Vue.set(vm.list, 0, 999)   // 正确方式
  • 初始化性能

在初始化Vue实例的时候,会递归遍历所有 data 属性,为每个属性设置 getter 和 setter。当处理大对象或深层嵌套结构时会带来严重的性能问题。

  • 内存占用

每个data属性都有一个依赖列表,占用了大量的内存。

二、Vue 3的响应式系统(基于Proxy)

Vue3 的响应式系统是基于 ES6 的Proxy API 实现的,相比 Vue2 的Object.defineProperty,它实现了更彻底的响应式能力。其核心原理可以概括为:通过 Proxy 代理目标对象,劫持对象的读、写、删除、添加属性等操作,完成依赖收集和触发更新,从而实现响应式的功能。

1.数据劫持

Vue 3 使用Proxy API创建代理对象,劫持data对象的各种操作,包括读、写、删除、添加属性等操作:

  • get:当访问数据时,收集依赖该属性的effect函数。
  • set:当修改、新增数据时,通知依赖该属性的effect函数重新计算。
  • deleteProperty:当删除数据时,通知依赖该属性的effect函数重新计算。

2.依赖收集

  • 维护一个全局的依赖映射表,存放所有响应式对象的所有响应式属性的依赖列表。
  • 当属性被读取时(比如在组件渲染、watch),触发get拦截器。
  • 将当前的effect函数添加到该属性的依赖列表中。

3.触发更新

当响应式对象的属性被修改、新增或删除时(触发set或deleteProperty拦截器),Vue3 会通知依赖该属性的所有effect函数重新执行,从而实现视图更新或监听响应。

4.核心代码

// 创建代理对象,劫持data对象的各种操作,包括读、写、删除、新增属性等操作:
function createReactiveObject(target) {
  const proxy = new Proxy(target,  {
    // 拦截读操作
    get(target, key, receiver) {
      // 依赖收集
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    // 拦截写、新增操作
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      // 触发更新
      if (newVal !== val) {
        trigger(target, key);
      }
      return result;
    }
    // 拦截删除操作
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      trigger(target, key);
      return result;
  }
  });
  return proxy;
}
// 全局的依赖映射表:target -> key -> effects
const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) return;
  
  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);
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => {
      if (effect.scheduler) {
        effect.scheduler();
      } else {
        effect.run();
      }
    });
  }
}
let activeEffect = null;

class ReactiveEffect {
  constructor(fn, scheduler = null) {
    this.fn = fn;
    this.scheduler = scheduler;
  }
  
  run() {
    activeEffect = this;
    const result = this.fn();
    activeEffect = null;
    return result;
  }
}

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.run();
  return _effect;
}
❌
❌