面试备战录

1、Vue 响应式实现原理

Vue2 利用 Object.defineProperty()对数据对象的属性进行"劫持"(数据劫持 / 数据代理)。

  1. 初始化响应式数据
    • Vue 在初始化时会遍历 data 中的每个属性,用 Object.defineProperty 将它们变成 getter/setter
    • 当你访问或修改这些属性时,会自动触发对应的 getter/setter。
  2. 依赖收集(Dep + Watcher)
  • 每个响应式属性都持有一个 Dep(依赖收集器),用于记录哪些组件/函数依赖了它。
  • 当组件渲染时,相关属性的 getter 会被访问,从而将 Watcher 注册到该属性的 Dep 中。
  1. 派发更新
  • 当属性的 setter 被调用(属性变更),会触发 Dep 通知 Watcher 执行更新操作(如重新渲染组件)。
  • Dep 就是"依赖记录本"
    • 每一个响应式属性(比如 msg)都有一个专属的 Dep 实例
    • 它内部维护着一个数组 subs,记录"谁用过我"。
    • 这个"谁"指的就是那些依赖当前属性的 Watcher(组件、计算属性、侦听器等)。
  • Watcher 是"观察者"或"执行者"
    • 它代表"正在运行的视图更新函数"或"计算逻辑"。
    • 比如一个组件渲染函数、一个 computed、一个 watcher 函数,它们都是 Watcher。
    • 当这些逻辑访问某个响应式数据时,Dep 就把这个 Watcher 收进自己的小本本。
  • getter 阶段的行为(收集依赖):
    • 就是把当前正在执行的 Watcher 记录到 Dep 中
  • setter 阶段的行为(触发更新):
    • 修改这个数据时,会触发 dep.subs(里面的所有 Watcher)的执行

扩展:

  1. Object.defineProperty 只劫持对象,对于基本数据类型不会进行劫持,因为 defineProperty 是对象上的属性;而基本数据类型的数据,会被使用{name:'张三'}包裹起来当做一个对象进行劫持,这个对象会挂载到当前组件实例上(vm)。
js 复制代码
// 只劫持对象
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return;
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}
  1. Vue2 中每个.vue 文件会被当做一个组件,在使用时都会被 Vue 实例成一个独立的组件实例(vm),它拥有自己完整的响应式系统、生命周期、作用域,多个组件之间互不影响。
  2. Vue 的组件实例是一个"树状结构",所有子组件实例最终都会挂载(嵌套)在一个根 Vue 实例 vm 上。
  3. Vue2 中每个组件内的 this 指向在初始时,都被指向了当前组件实例。
  4. Vue2 组件里面的 data 要写成函数而不是对象,是为了避免数据被多个实例共享。
  5. Vue 2 对对象做响应式劫持时,是递归遍历的(深度遍历),确实会劫持所有层级的属性,哪怕你并不一定会用到它们。
js 复制代码
function defineReactive(obj, key, val) {
  observe(val); // 递归:如果 val 是对象,也变成响应式

  Object.defineProperty(obj, key, {
    get() {
      // 做依赖收集
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        observe(newVal); // 如果赋的新值是对象,也递归处理
        // 通知依赖更新
      }
    }
  });
}

Object.defineProperty 优点:

  • 兼容性极好(ES5 标准)
    • Object.defineProperty 是 ES5 标准,支持范围非常广,基本兼容所有主流浏览器(包括 IE9+)
    • 这让 Vue 2 在广泛的浏览器环境中都能稳定运行。
  • 性能相对较好
    • 它是在属性层面劫持,性能开销较低,尤其是对于单层属性的读取和赋值。
    • 不像 Proxy,需要代理整个对象,某些场景下 defineProperty 反而更轻量。
  • 实现相对直接
    • 通过给每个属性定义 getter/setter,劫持数据变化,思想简单直接;
    • 代码逻辑清晰,容易理解"数据劫持"的核心机制。
  • 内存占用较低:
    • 只对已有属性添加访问器,不需要额外代理对象,相对内存消耗更小。
  • 细粒度控制
    • 你可以针对每个具体属性设置不同的 getter/setter,灵活度高。

Object.defineProperty 缺点:

  • 只能劫持"已有属性",不能监听新增或删除的属性;
  • 必须递归地遍历所有嵌套属性,成本高、性能差;(嵌套越深、数据越大,初始化越慢,内存占用也越高)
  • 无法监听数组索引变动和长度变化(Vue 2 的数组响应式,是对数组原型做"函数重写"(push、pop、splice 等),并在这些方法内部手动触发视图更新。)
  • 无法观察对象属性的访问顺序 / 精细控制访问路径
  • 代码维护复杂,边界情况多,hack 多(Vue 2 的响应式实现基于 Object.defineProperty,要给对象的每个属性都写一遍 getter/setter,代码逻辑比较繁琐,尤其是要递归遍历嵌套对象,代码量和复杂度都较大。)

Vue 3 响应式原理(基于 Proxy ES6 引入)

  • 使用 Proxy 代理整个对象
    • 通过 reactive() 方法将整个对象用 Proxy 包装。
    • 能够拦截所有操作(get/set/delete 等),更灵活且支持动态属性。
  • 依赖收集(effect + targetMap)
    • Vue3 用 effect 替代 Vue2 中的 Watcher。
    • 所有响应式对象的依赖关系会存储在一个全局的 targetMap 中
  • 更新触发
    • 当 Proxy 拦截到属性的变更(set),会查找该属性对应的 effect 并执行,实现视图更新。
  • Proxy(Vue3)
    • 相当于定义的响应式数据,都是被 Proxy 进行包裹过一层;
    • 副作用函数(effect)触发渲染,读取的响应式数据;
    • 收集依赖就是 Vue 记录下某个响应式数据被哪些副作用函数(effect)用到了
    • 触发更新就是当某个响应式数据被修改时,Vue 会找到「依赖这个数据的副作用函数(effect)」,然后执行这些 effect,完成视图或逻辑的更新。
js 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key); // 收集依赖
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key); // 触发更新
      return result;
    }
  });
}

扩展:

  1. Proxy 是一个内置的构造函数(类),可以代理整个对象;原生提供的代理功能,只针对一层对象;
  2. 懒代理(lazy reactive):懒代理(lazy reactive)并不是 Proxy 自身内建的机制,而是 Vue 3 响应式系统基于 Proxy 设计的一个策略。
  3. Vue 3 的响应式库,在 get 拦截器里检测到访问的属性值是个对象时,会主动给这个值再次包一层 Proxy,实现"按需代理"。
  4. Vue 3 用 Proxy 的 get 拦截做递归调用,实现深层响应式,且避免了 Vue 2 那种初始化时递归所有层级的性能问题。

Proxy优点:

  • 可以监听新增/删除的属性
  • 支持数组索引、length
  • 无需递归劫持整个对象树(按需代理)
    • Vue2 会在一开始 递归遍历整个 data 对象,用 defineProperty 把每个属性都设置getter/setter。
      • 初始化性能差(对象大了非常慢)
      • 有些从未访问过的字段也被劫持了
    • Vue3 是"懒代理",只有当你访问到 state.user.info.address 时,它才会"递归代理"进去,性能提升明显。
  • 可以代理整对象,而不是一个个属性
    • defineProperty 是属性级别的劫持,得一个个写
    • Proxy 是对象级别的代理,一次包住整个对象
  • 更强大的拦截能力(支持 13 种操作)
    • get、set、deleteProperty、has、ownKeys、getOwnPropertyDescriptor、defineProperty、preventExtensions
  • 响应式系统更通用、更简洁
    • Vue3 中所有响应式的功能(如 reactive, ref, computed, watchEffect)本质都依赖于 Proxy + effect,设计更加统一简洁易于扩展。
  • 更利于 Tree-shaking,支持更小体积打包
    • Vue2 的响应式是以 Observer 和 Watcher 类为核心,不容易被 tree-shaking。
    • 而 Vue3 的响应式系统更像函数式 reactive, effect, track, trigger 等都是纯函数,支持按需裁剪 ➜ 更小体积。 Proxy缺点:
  • 兼容性相对 Vue2 差
    • Proxy 是 ES6 的新特性,不支持在旧版浏览器(尤其是 IE11)中运行。
    • Vue3 官方明确不支持 IE11,这对某些老项目是个障碍。
    • Vue2 使用 Object.defineProperty,可以兼容 IE11。
  • 需要全对象代理(不能只代理某个属性)
    • Proxy 是代理整个对象,不能"只劫持某个属性"。
    • 所以必须用一个"统一的入口"来包裹整个数据对象(比如 reactive()),这会让对部分属性响应式处理变复杂。
  • 无法拦截对象内部访问路径
    • 比如你访问了 state.user.info.name,你没办法知道这个访问路径是从 state → user → info → name 进来的。
    • 带来的问题:无法记录访问路径(不像 MobX 有"路径追踪"能力);也就无法做到某些依赖图精确的性能分析
  • 性能可能差于 defineProperty(在一些简单场景),虽然 Proxy 懒劫持大大提升了深层对象初始化性能,但:
    • 在某些浅层、结构简单的对象中,defineProperty 手动控制颗粒度更小,开销可能反而更低。
    • Proxy 的 handler 执行逻辑会有一定的性能开销(特别是频繁触发 get/set 的场景)。
  • WeakMap 缓存不易调试或手动清理
    • Vue3 使用了 WeakMap 做响应式对象缓存(防止重复代理):const reactiveMap = new WeakMap();
    • 这虽然高效,但不容易调试(看不到里面内容)
    • 手动清理或强制刷新响应式状态比较麻烦
  • 调试难度稍高
    • Proxy 的响应式是通过代理对象"偷偷做事"的,一般:
      • 数据变了,视图就更新了,但你不知道什么时候 trigger 的
      • 这导致新手更难调试响应式问题
  • 某些库对 Proxy 不友好
    • 一些第三方库(尤其是老旧库)无法识别 Proxy 对象,会导致 bug。
    • 比如序列化、深拷贝、类型判断等可能出错:
相关推荐
前端菜鸟杂货铺5 分钟前
前端首屏优化及可实现方法
前端
遂心_5 分钟前
React Fragment与DocumentFragment:提升性能的双剑合璧
前端·javascript·react.js
ze_juejin5 分钟前
ionic、flutter、uniapp对比
前端
咚咚咚ddd6 分钟前
WebView Bridge 跨平台方案:统一 API 实现多端小程序通信
前端·前端工程化
程序视点6 分钟前
Microsoft .Net 运行库离线合集包专业解析
前端·后端·.net
混水的鱼7 分钟前
PasswordValidation 密码校验组件实现与详解
前端·react.js
ze_juejin8 分钟前
async、defer 和 module 属性的比较
前端
归于尽10 分钟前
关于数组的这些底层你真的知道吗?
前端·javascript
puppy0_012 分钟前
前端性能优化基石:HTML解析与资源加载机制详解
前端·性能优化
三小河12 分钟前
e.target 和 e.currentTarget 的区别
前端