前端开发中,“响应式”是实现数据与视图同步的核心能力。这种能力我首先想到是Vue的双向绑定,那就看看其是如何实现的

一、Vue2中

  • Vue 2 主要通过 Object.defineProperty对数据进行劫持,监听属性的读取(get)和修改(set)操作,结合依赖收集和触发更新的机制实现双向绑定。

1. 核心工具:Object.defineProperty

Object.defineProperty(obj, prop, descriptor)可以给对象的属性添加 getter(读取时触发)和 setter(修改时触发),从而拦截对属性的访问和修改操作。

2. 关键模块

  • ​Observer(观察者)​​:负责递归遍历对象的所有属性,为每个属性添加 getter 和 setter,实现数据劫持。

  • ​Dep(依赖收集器)​​:每个属性对应一个 Dep,用于收集所有依赖该属性的 Watcher(观察者)。

  • ​Watcher(观察者)​​:作为桥梁,连接 Dep 和视图/计算属性等。当数据变化时,Dep 会通知所有关联的 Watcher 触发更新。

3. 具体流程

  • 数据劫持

    • Vue 实例创建时,会对 data中的对象进行递归遍历,为每个属性调用 Object.defineProperty添加 getter 和 setter:

function defineReactive(obj, key, val) {
  const dep = new Dep(); // 每个属性对应一个 Dep
  Object.defineProperty(obj, key, {
    get() {
      // 读取属性时,收集依赖(将当前 Watcher 加入 Dep)
      if (Dep.target) { 
        dep.depend(); 
      }
      return val;
    },
    set(newVal) {
      if (val === newVal) return; // 值未变化时不触发更新
      val = newVal;
      // 修改属性时,通知所有依赖的 Watcher 更新
      dep.notify(); 
    }
  });
}
  • 依赖收集

    • 当视图渲染(如 {{ message }}v-model)时,会创建对应的 Watcher。Watcher 在初始化时会读取相关数据(触发 getter),此时 Dep 会将当前 Watcher 记录为自己的依赖(即该视图依赖这个数据)。

  • 数据修改触发更新

    • 当数据被修改时(如 this.message = 'new value'),会触发 setter,通知 Dep 调用所有关联 Watcher 的 update方法。Watcher 收到通知后,会重新渲染视图,从而实现数据到视图的同步。

二、Vue 3

  • Vue 3 为了更高效地处理对象和数组的响应式,以及支持更复杂的场景(如深层对象、动态新增属性),改用 Proxy替代 Object.defineProperty

1. 核心工具:Proxy

Proxy可以代理整个对象,拦截对象的各种操作(如读取、修改、删除属性,甚至遍历等),比 Object.defineProperty更灵活和全面。

2. 关键升级点

  • 无需递归初始化​​:Proxy代理的是整个对象,只有在访问具体属性时才会触发拦截,因此不需要在初始化时递归遍历所有子属性(懒加载)。

  • 支持数组的原生操作​​:Object.defineProperty需要重写数组的 push/pop/shift/unshift等方法来触发更新,而 Proxy可以直接拦截数组的操作。

  • 支持动态新增/删除属性​​:Object.defineProperty无法检测对象新增或删除属性(需手动调用 Vue.set/Vue.delete),而 Proxy可以通过拦截 setdeleteProperty操作实现响应式。

3. 核心逻辑(简化版)

Vue 3 的响应式系统核心是 reactive函数,返回一个 Proxy 代理对象:

function reactive(target) {
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  const handler = {
    get(target, key, receiver) {
      // 收集依赖(类似 Vue 2 的 Dep)
      track(target, key);
      // 递归代理子属性(实现深层响应式)
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      // 触发更新(类似 Vue 2 的 notify)
      trigger(target, key);
      return Reflect.set(target, key, value, receiver);
    }
  };
  return new Proxy(target, handler);
}

4. 依赖收集与触发

Vue 3 用 Effect函数替代了 Watcher,通过 track(收集依赖)和 trigger(触发更新)函数管理依赖关系:

  • 当读取响应式数据时(如模板渲染或计算属性),调用 track记录当前正在执行的 Effect

  • 当修改响应式数据时(如赋值操作),调用 trigger找到所有关联的 Effect并重新执行,从而更新视图

总的来说,Vue 的双向绑定通过 ​​数据劫持​​ 实现对数据的监听,通过 ​​依赖收集​​ 确定哪些视图依赖该数据,最终通过 ​​发布-订阅模式​​ 在数据变化时触发视图更新,同时通过 v-model等指令绑定视图事件,实现视图到数据的反向更新。

看完Vue的实现方式,顺便看看最近在使用的MST(MobX-State-Tree​)中是如何实现响应式的

三、MST( ​​MobX-State-Tree​​)文档

1. 核心工具:Proxy

  • MST和Vue一样 的响应式能力均依赖 ES6 的 Proxy两者通过 Proxy拦截对象的操作(如属性读取、修改、删除等),实现对数据变化的追踪和响应。

  • MST 的状态树(State Tree)由多个“节点”(Nodes)组成,每个节点都是一个被 Proxy代理的可观察对象。MST 的 Proxy不仅追踪数据变化,还额外记录状态的“类型定义”(如模型字段的类型、动作(Actions)的约束等),以实现更复杂的状态管理逻辑(如状态校验、历史回溯、持久化等)

2. 关键模块

  • MST 的关键模块围绕“​​结构化状态管理​​”展开,通过:

    • 依赖追踪:依赖追踪是响应式的核心:当数据被访问时,需要知道“哪些逻辑依赖它”,以便数据变化时触发这些逻辑。MST 的依赖追踪是​​显式结构关联​​的:状态的修改会触发与该状态节点关联的“副作用”(如 watchactions中的逻辑),但依赖关系由状态的类型定义和 action的作用域显式决定。

    • 不可变性:MST 强制要求​​所有状态修改必须通过 action​(类似 Redux 的 reducer),确保修改逻辑集中且可调试。action可以被标记为 async,支持异步操作,且内部修改状态时会自动触发响应式更新。

    • 状态快照与时间旅行:MST 内置了​​状态快照(Snapshot)​​和​​时间旅行(Time Travel)​​能力,通过 onSnapshot监听状态变化并记录历史,方便调试和回滚状态。

使用示例

import { types } from 'mobx-state-tree';

// 定义子模型:地址
const Address = types.model({
  city: types.string,
  street: types.string,
  zipCode: types.string
}).actions(self => ({
  updateCity(newCity: string) {
    self.city = newCity; // 通过 action 修改子模型状态
  }
}));

// 定义主模型:用户
const User = types.model({
  id: types.identifier, // 标识符(唯一键)
  name: types.string,
  age: types.number,
  address: Address, // 嵌套子模型
  hobbies: types.array(types.string) // 字符串数组
}).props({
  // 可选:静态属性(不可修改)
  createdAt: types.string
}).defaults({
  // 默认值(工厂函数)
  age: 18,
  hobbies: [] as string[]
});


// 创建模型实例(状态树节点)
const user = User.create({
  id: 'u1',
  name: 'Alice',
  address: { city: 'Beijing', street: 'Main St', zipCode: '100000' },
  createdAt: new Date().toISOString()
});