Vue 2与Vue 3响应式原理的对比与实现

如果你用过Vue,你一定对它那响应式系统印象深刻。你只需要在<script>里修改一个数据,template里对应的视图就会自动更新。

JavaScript 复制代码
// 你只做了这个
this.message = 'Hello, World!';

整个过程很丝滑流畅。

但作为工程师,我们都知道,它正是Vue框架的精髓所在,也是Vue 2和Vue 3在底层实现上最核心的区别。

这篇文章,就让我们一起深入底层,不仅要对比Vue 2的Object.defineProperty和Vue 3的Proxy这两种方案的优劣,更要亲手用几十行代码,分别实现它们的最简原型。


Vue 2 的 Object.defineProperty

Vue 2的响应式系统,是基于Object.defineProperty这个ECMAScript 5的API。

它的核心思想是:劫持对象属性的gettersetter来实现。

你可以把它想象成,我们给一个对象的每个"属性" 都派驻了一个"对象守卫"。

  • 当有人要读取这个属性时(get),并悄悄记下"谁"(我们称之为依赖)来过。
  • 当有人要修改这个属性时(set),立刻去通知所有之前的依赖:"嘿嘿数据更新了!"

1. 动手实现一个迷你版

我们来用代码模拟这个过程。首先,我们需要一个"依赖管理器"(Dep)和一个"依赖"(Watcher)。

JavaScript 复制代码
// 依赖管理器:每个属性都有一个,用来存放所有依赖它的Watcher
class Dep {
  constructor() {
    this.subscribers = new Set(); // 用Set来存放Watcher,可以自动去重
  }

  // 添加依赖
  depend() {
    if (Dep.target) {
      this.subscribers.add(Dep.target);
    }
  }

  // 通知更新
  notify() {
    this.subscribers.forEach(sub => sub.update());
  }
}
// 全局的临时变量,用来存放当前正在计算的Watcher
Dep.target = null;

// 依赖:一个需要响应数据变化的对象(比如一个渲染函数)
class Watcher {
  constructor(updateFn) {
    this.updateFn = updateFn;
  }
  
  // 包装一下,设置Dep.target,然后执行更新函数
  // 执行更新函数时,就会触发属性的get,从而被Dep收集起来
  watch() {
    Dep.target = this;
    this.updateFn();
    Dep.target = null;
  }

  update() {
    this.updateFn();
  }
}

现在,我们来写核心的"劫持"函数:

JavaScript 复制代码
// 核心函数:为对象的单个属性添加响应式
function defineReactive(obj, key, val) {
  const dep = new Dep(); // 每个属性都有一个自己的依赖管理器

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 依赖收集
      dep.depend();
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 通知依赖更新
      dep.notify();
    }
  });
}

// 遍历对象,对所有属性进行劫持
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return;
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

2. Object.defineProperty的"先天不足"

这个方案很巧妙,但在实际使用中暴露了几个无法根治的问题:

  • 无法监听对象属性的新增和删除Object.defineProperty只能劫持一个对象已有的 属性。如果你在初始化之后,给对象动态添加一个新属性(obj.newProp = '...'),Vue 2是无法检测到这个变化的。为此,Vue 2不得不提供一个特殊的API------Vue.set来解决这个问题。
  • 无法原生监听数组的变化 :它不能直接劫持数组的索引和length属性。Vue 2的解决方案是"魔改"了数组的原型方法,比如pushpopsplice等,在这些方法被调用时,手动去触发更新。这既不优雅,也增加了额外的开销。
  • 必须在初始化时就递归遍历所有属性observe函数需要在最开始就把一个对象(可能非常深、非常大)的所有属性都用Object.defineProperty包装一遍,这对性能是有一定影响的。

Vue 3 的 Proxy

为了解决Object.defineProperty的这些"先天不足",Vue 3毅然决然地拥抱了ES6的新特性------Proxy

Proxy的核心思想是:在目标对象之前架设一层"代理",对对象的 所有操作 (不只是getset)进行拦截。

(概念说的烂透了,不想继续写了🤭)

1. 动手实现一个迷你版

使用Proxy,代码会变得更简洁、更强大。我们只需要一个reactive函数。

JavaScript 复制代码
// (这里的Dep和Watcher可以复用上面的)

function reactive(obj) {
  // 用一个Map来存储每个原始对象和它的依赖管理器
  const targetMap = new WeakMap();

  return new Proxy(obj, {
    get(target, key, receiver) {
      // 依赖收集
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
      }
      let dep = depsMap.get(key);
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
      }
      dep.depend();
      
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      
      // 通知更新
      let depsMap = targetMap.get(target);
      if (depsMap) {
        let dep = depsMap.get(key);
        if (dep) {
          dep.notify();
        }
      }
      
      return result;
    }
    // deleteProperty, has等其他trap可以类似实现
  });
}

(注:这是一个极简实现,真实的Vue实现会更复杂,会用tracktrigger函数来封装依赖收集和通知逻辑)

2. Proxy的"降维打击"优势

  • 全方位拦截Proxy可以拦截多达13种操作,包括属性的增、删、查等,完美解决了Object.defineProperty无法监听属性新增/删除的问题。再也不需要Vue.set了。
  • 原生支持数组 :对数组的操作(包括通过索引修改、修改length)都能被Proxyget/set trap捕获,不再需要去"魔改"数组方法。
  • 懒处理,性能更佳Proxy是对整个对象的代理,它不需要在初始化时就去递归遍历所有属性。只有当你真正访问到某个深层属性时,才会触发对应的get操作,这是一种"懒代理"模式,在某些场景下性能更好。

没有对比 没有伤害👇

特性 Object.defineProperty (Vue 2) Proxy (Vue 3)
拦截目标 对象的单个属性 整个对象
新增/删除属性 不支持 (需Vue.set/delete) 支持
数组操作 不支持 (需重写原型方法) 支持
初始化 需递归遍历所有属性 只代理顶层对象,懒处理
浏览器兼容性 兼容到IE9 ES6+ (不兼容IE)

Object.definePropertyProxy,这不仅仅是一次简单的API替换,更是一次 Vue响应式系统的进化Proxy让Vue的响应式系统摆脱了历史的束缚,变得更加健壮、完整和高效。

虽然Proxy放弃了对IE的兼容,但这无疑是一个完全正确和值得的决策。

希望通过这次的对比和动手实现,能给你更深刻的理解。

分享完毕😊

相关推荐
一斤代码2 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子3 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina3 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路3 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_4 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说4 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding4 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js