面试备战录

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。
    • 比如序列化、深拷贝、类型判断等可能出错:
相关推荐
i听风逝夜26 分钟前
Web 3D地球实时统计访问来源
前端·后端
iMonster30 分钟前
React 组件的组合模式之道 (Composition Pattern)
前端
呐呐呐呐呢38 分钟前
antd渐变色边框按钮
前端
元直数字电路验证1 小时前
Jakarta EE Web 聊天室技术梳理
前端
wadesir1 小时前
Nginx配置文件CPU优化(从零开始提升Web服务器性能)
服务器·前端·nginx
牧码岛1 小时前
Web前端之canvas实现图片融合与清晰度介绍、合并
前端·javascript·css·html·web·canvas·web前端
灵犀坠1 小时前
前端面试八股复习心得
开发语言·前端·javascript
9***Y481 小时前
前端动画性能优化
前端
网络点点滴1 小时前
Vue3嵌套路由
前端·javascript·vue.js
牧码岛1 小时前
Web前端之Vue+Element打印时输入值没有及时更新dom的问题
前端·javascript·html·web·web前端