为什么代理比深度遍历快

6 阅读3分钟

这是一个非常经典的前端面试题,答案的核心落脚于 **“初始化性能”**和 “内存占用”

严格来说,并不是代理本身的指令执行速度比遍历快,而是代理的“懒加载”机制使得应用启动/对象创建时的开销远小于深度遍历

具体可以从以下三个维度来对比:

1. 时间维度:初始化成本(关键差距)

这是“快”的主要来源。

  • 深度遍历(如 Vue 2 的 Object.defineProperty: 在组件创建时,必须执行一次全量递归。如果对象层级很深(如 a.b.c.d.e.f = 1),引擎需要同步调用栈不断加深,阻塞 UI 渲染,直到把整棵对象树的每一个叶子属性都加上 getter/setter

  • 代理(如 Vue 3 的 Proxy: 只在根对象套一层外壳,递归过程被延迟到了访问时。初始化瞬间完成,只有当你真的去读 obj.a.b.c 时,才会临时去创建中间对象的代理。

结论:对于一个拥有 10,000 个深层属性的对象,深度遍历需要循环/递归 10,000 次并定义属性;代理只需要执行 1 次 new Proxy(target)

2. 内存维度:节约大量闭包

  • 深度遍历:为了拦截修改,通常需要在每个属性上挂载一个独立的 Dep(依赖收集器)闭包。对象的层级越深,内存占用越大
  • 代理:拦截器只有一个 handler 对象。依赖关系是存储在全局 WeakMap 中的,不需要为每一个未被访问的属性预先分配内存

3. 运行维度:惰性陷阱(Lazy Trap)

  • 深度遍历:对于你永远不访问的属性(比如后端返回了大而全的数据,但视图只展示了 user.name),深度遍历在初始化时浪费了性能去劫持那些无用字段。
  • 代理:由于是拦截操作符,不访问 = 不执行 = 零开销

4. 对“快”的误解澄清:运行时性能

虽然初始化快,但单论读写属性的一瞬间Proxy 的拦截器触发比直接的 getter/setter 函数调用稍慢(涉及 Reflect 调用和 handler 逻辑)。

阶段深度遍历代理
初始化/页面启动很慢 (阻塞递归)极快
深层属性首次访问瞬间 (已劫持好)稍慢 (需创建子代理)
重复读写属性稍快 (函数调用路径短)稍慢 (引擎优化难度略高)

总结对比表

对比维度深度遍历 (Object.defineProperty)代理 (Proxy)
工作方式主动递归,必须遍历到底。被动拦截,访问到哪算哪。
对巨大对象的影响初始化时间随属性数量 O(n) 增加。初始化时间 O(1) 恒定。
数组处理需要重写 7 种数组方法,且需遍历索引。原生支持 length 和索引变化,无需遍历。
视觉比喻一本新书必须把所有页都翻一遍才能合上。只有当你翻到某一页时,那一页才会发光。

所以,回答“为什么快”的简洁答案:

因为代理是“按需劫持”(Lazy Observation),而深度遍历是“全量劫持”(Eager Observation)。初始化时节省了大量的递归和内存开销。