Reader

尤雨溪搞响应式为什么要从 Object.defineProperty 换成 Proxy❓

| 掘金本周最热 | Default

前言

你说,为什么❓尤雨溪搞响应式,他为什么要换掉Object.defineProperty呢❓

proxy什么来头❓

有一次👀看他直播,说去面试人家问他原型链,他不会,GG了面试黄了,你说他是不是无中生有暗度陈仓凭空想象凭空捏造new Proxy来换掉Object.defineProperty的呢?

还真不是,尤雨溪的响应式,我们暂且叫成插一脚吧👇,请听我细细道来👂

在前端开发中,响应式系统是现代框架的核心特性。无论是 Vue 还是 React,它们都需要实现一个基本功能:当数据变化时,自动更新相关的视图。用通俗的话说,就是要在数据被读取或修改时"插一脚",去执行一些额外的操作(比如界面刷新、计算属性重新计算等)。

// 读取属性时
obj.a; // 需要知道这个属性被读取了

// 修改属性时
obj.a = 3; // 需要知道这个属性被修改了

但原生 JavaScript 对象不会告诉我们这些操作的发生。那么,尤雨溪是如何实现这种"插一脚"的能力的呢?

正文

Vue 2 的"插一脚"方案 - Object.defineProperty

基本实现原理

Vue 2 使用的是 ES5 的 Object.defineProperty API。这个 API 允许我们定义或修改对象的属性,并为其添加 getter 和 setter。

const obj = { a: 1 };

let v = obj.a;
Object.defineProperty(obj, 'a', {
  get() {
    console.log('读取 a'); // 插一脚:知道属性被读取了
    return v;
  },
  set(val) {
    console.log('更新 a'); // 插一脚:知道属性被修改了
    v = val;
  }
});

obj.a;     // 输出"读取 a"
obj.a = 3; // 输出"更新 a"

完整对象监听

为了让整个对象可响应,Vue 2 需要遍历对象的所有属性:

function observe(obj) {
  for (const k in obj) {
    let v = obj[k];
    Object.defineProperty(obj, k, {
      get() {
        console.log('读取', k);
        return v;
      },
      set(val) {
        console.log('更新', k);
        v = val;
      }
    });
  }
}

处理嵌套对象

对于嵌套对象,还需要递归地进行观察:

function _isObject(v) {
  return typeof v === 'object' && v !== null;
}

function observe(obj) {
  for (const k in obj) {
    let v = obj[k];
    if (_isObject(v)) {
      observe(v); // 递归处理嵌套对象
    }
    Object.defineProperty(obj, k, {
      get() {
        console.log('读取', k);
        return v;
      },
      set(val) {
        console.log('更新', k);
        v = val;
      }
    });
  }
}

Vue 2 方案的两大缺陷

缺陷一:效率问题

在这种模式下,他就必须要去遍历这个对象里边的每一个属性...这是第一个缺陷:必须遍历对象的所有属性,对于大型对象或深层嵌套对象,这会带来性能开销。

缺陷二:新增属性问题

无法检测到对象属性的添加或删除:

obj.d = 2; // 这个操作不会被监听到

因为一开始遍历的时候没有这个属性,后续添加的属性不会被自动观察。

Vue 3 的"插一脚"方案 - Proxy

基本实现原理

Vue 3 使用 ES6 的 Proxy 来重构响应式系统。Proxy 可以拦截整个对象的操作,而不是单个属性。

const obj = { a: 1 };

const proxy = new Proxy(obj, {
  get(target, k) {
    console.log('读取', k); // 插一脚
    return target[k];
  },
  set(target, k, val) {
    if (target[k] === val) return true;
    console.log('更新', k); // 插一脚
    target[k] = val;
    return true;
  }
});

proxy.a;     // 输出"读取 a"
proxy.a = 3; // 输出"更新 a"
proxy.d;     // 输出"读取 d" - 连不存在的属性也能监听到!

完整实现

function _isObject(v) {
  return typeof v === 'object' && v !== null;
}

function reactive(obj) {
  const proxy = new Proxy(obj, {
    get(target, k) {
      console.log('读取', k);
      const v = target[k];
      if (_isObject(v)) {
        return reactive(v); // 惰性递归
      }
      return v;
    },
    set(target, k, val) {
      if (target[k] === val) return true;
      console.log('更新', k);
      target[k] = val;
      return true;
    }
  });
  return proxy;
}

Proxy 的优势

  1. 无需初始化遍历:直接代理整个对象,不需要初始化时遍历所有属性
  2. 全面拦截:可以检测到所有属性的访问和修改,包括新增属性
  3. 性能更好:采用惰性处理,只在属性被访问时才进行响应式处理
  4. 更自然的开发体验:不需要特殊 API 处理数组和新增属性

"proxy 它解决了什么问题?两个问题。

第一个问题不需要深度遍历了,因为它不再监听属性了,而是监听的什么?整个对象。

同时也由于它监听了整个对象,就解决了第二个问题:能监听这个对象的所有操作,包括你去读写一些不存在的属性,都能监听到。"

原理对比与源码解析

原理对比

特性Object.definePropertyProxy
拦截方式属性级别对象级别
新增属性检测不支持支持
性能初始化时需要遍历按需处理
深层嵌套处理初始化时递归处理访问时递归处理

源码实现差异

Vue 2 实现

  • src/core/observer 目录下
  • 初始化时递归遍历整个对象
  • 需要特殊处理数组方法

Vue 3 实现

  • 独立的 @vue/reactivity
  • 使用 Proxy 实现基础响应式
  • 惰性处理嵌套对象
  • 更简洁的 API 设计

为什么 Proxy 是更好的选择?

  1. 更全面的拦截能力:可以拦截对象的所有操作,包括属性访问、赋值、删除等
  2. 更好的性能:不需要初始化时递归遍历整个对象
  3. 更简洁的 API:不再需要 Vue.set/Vue.delete 等特殊 API
  4. 更自然的开发体验:开发者可以使用普通的 JavaScript 语法操作对象

总结

需显式操作(defineProperty)-> 声明式编程(Proxy)

局部监听(属性级别)-> 全局拦截(对象级别)

从 Object.defineProperty 到 Proxy 的转变,不仅是 API 的升级,更是前端框架设计理念的进步。Vue 3 的响应式系统通过 Proxy 实现了更高效、更全面的数据监听。