Vue2 的 defineProperty 差在哪四层追问筛掉九成候选人

能背出「Vue3 用 Proxy 代替 defineProperty」只值 0 分。讲清 Vue2 哪些场景天生处理不了、Proxy 又漏了哪些坑、Vue3 为什么留着 markRaw,才算及格。

一句正确的废话,值 0 分

「Vue2 和 Vue3 的响应式有什么区别?」

候选人张口就来:Vue2 用 Object.defineProperty,Vue3 用 Proxy

没错,但这是句废话。好比问 MySQL 和 PostgreSQL 的区别,你答「一个用 B+ 树一个也用 B+ 树」------你报的是 API 名字,不是工程判断。

真正该往下追问三层:defineProperty 的硬伤是什么?Proxy 补了哪些线上坑?换完之后,还有哪些场景 Proxy 也绕不开?

defineProperty 的签名就写死了它的上限

Vue2 通过 Object.defineProperty 把 data 上的每个属性转成 getter/setter,读取时收集依赖,赋值时触发更新。

js 复制代码
function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend()        // 收集当前 Watcher
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      dep.notify()        // 通知更新
    }
  })
}

关键限制藏在 defineReactive 的签名里:它一次只能处理一个 key,而且是调用那一刻已经存在的 key。

所以 obj.newField = 1delete obj.field 这两个操作,defineProperty 根本没机会介入。

四个硬伤,两个是数组,两个是对象

三年经验的人在这一层开始模糊。具体是四条:

  1. 数组下标赋值监听不到arr[0] = 'x' 不会触发 setter,因为 Vue2 压根没给数组下标做 defineProperty
  2. 数组长度修改监听不到arr.length = 0 同理
  3. 对象新增、删除属性监听不到 :所以才有 Vue.set / this.$$setVue.delete
  4. 只支持普通对象和数组:Map、Set、WeakMap 这类原生集合类型完全无能为力

前两条的补丁是重写七个数组原型方法:

js 复制代码
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

methodsToPatch.forEach(method => {
  const original = Array.prototype[method]
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    if (method === 'push' || method === 'unshift') inserted = args
    else if (method === 'splice') inserted = args.slice(2)
    if (inserted) ob.observeArray(inserted)  // 新插入的元素也要变成响应式
    ob.dep.notify()
    return result
  })
})

很多人知道要写 this.$$set,但说不出为什么非它不可------因为 defineProperty 的拦截能力在定义那一刻就封死了。

来看 Vue2 的完整数据流,注意那条断掉的支路:

flowchart TD A[render 函数执行] -->|读取 key| B[defineProperty getter] B --> C[Dep.depend 收集 Watcher] D[修改已有 key] -->|赋值| E[setter] E --> F[Dep.notify] F --> G[scheduler 异步更新队列] G --> A H[新增 key] -->|无拦截点| I[响应式丢失 视图不更新] J[删除 key] -->|无拦截点| I K[arr 下标赋值] -->|无拦截点| I

Proxy 填了旧坑,自己又挖了新坑

Proxy 代理的是整个对象,不是某个 key,所以天生能拦截 13 种操作。

js 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key)
      const res = Reflect.get(target, key, receiver)
      return isObject(res) ? reactive(res) : res  // 懒代理,访问到才递归
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) trigger(target, key)  // 新增 key 也走这里
      return result
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey) trigger(target, key)
      return result
    }
  })
}

对比一下两边到底谁修好了谁:

场景 Vue2 defineProperty Vue3 Proxy
新增属性 不触发,要 $$set 原生触发
删除属性 不触发,要 $$delete 原生触发
数组下标赋值 不触发 原生触发
数组长度修改 不触发 原生触发
Map / Set 不支持 支持
初始化开销 递归遍历所有 key 懒代理,访问才递归
IE11 兼容 支持 不支持

Reflect 存在的意义不是仪式感,是保证 receiver 正确传递 ,让 this 在继承和代理链里指向代理对象而不是原始对象。少了它,super 访问和 getter 里的 this 都会指错。

那 Proxy 就没坑了?想得美,至少三类:

js 复制代码
const state = reactive({ count: 0, nested: { a: 1 } })

// 1. 解构:拿到的是普通 number,跟响应式断联
let { count } = state
count++  // 视图不动

// 2. 展开:copy 是全新的普通对象
const copy = { ...state }

// 3. 直接改原始对象:Proxy 是包装,不是克隆
const raw = { a: 1 }
const p = reactive(raw)
raw.a = 2   // 绕过代理,不触发更新

还有 ref 传原始值、reactive 不接受基础类型------因为 Proxy 的 target 必须是对象,Proxy(1, {}) 直接抛 TypeError 。这就是 Ref 存在的全部理由:给原始值套一层 { value } 的壳,再对这个壳做代理。

flowchart TD A[响应式入口] --> B[数据类型判断] B -->|对象 数组 Map Set| C[reactive 走 Proxy] B -->|string number boolean| D[Ref 包一层 value 对象] D --> E[get value 时 track 收集] D --> F[set value 时 trigger 派发] C --> G[嵌套对象访问时才递归代理] D --> H[value 是对象则内部再 reactive]

markRaw 不是摆设,是性能账

到这一层才叫懂框架。

为什么 Vue3 还留着 markRaw 因为不是所有数据都值得被代理。一个 5000 行的 ECharts options、一个 Frozen 的配置常量、一个第三方库的实例对象------把它们塞进 reactive,Proxy 的 get/set 拦截会在每次访问时叠加开销。

js 复制代码
import { markRaw } from 'vue'

const state = reactive({
  chart: markRaw(echarts.init(dom))  // 打标记,跳过代理
})

markRaw 的实现就是加个标志位:

js 复制代码
export function markRaw(value) {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

reactive 内部一看到 SKIP 就直接返回原对象。这也是为什么 shallowRef / shallowReactive 在大数据列表场景是刚需------几万条数据的列表,全量递归代理的内存开销和 GC 压力都不是小数目。

性能账也要算清楚:

  • 初始化:Vue2 必须递归遍历 data 的所有 key 打 defineProperty,对象越大启动越慢;Vue3 是懒代理,访问到哪层代理哪层
  • 更新:Vue2 每次 set 直接打 dep;Vue3 要多走一层 Proxy 的 trap + Reflect,单次访问略慢,但省掉了递归初始化的巨坑
  • 兼容性 :Proxy 是浏览器原生 API,IE 完全不支持,且无法 polyfill------这是 Vue3 彻底放弃 IE11 的核心原因,不是团队懒,是能力边界

下次这么答

再被问到这道题,别急着报 API 名字。按这个顺序说:defineProperty 只能拦截已有 key,所以数组下标、对象增删、Map/Set 全都漏;Proxy 代理整个对象把这些补上,代价是原始值的 Ref 包装、解构和展开的断联、以及 IE 的彻底出局。

面试官想听的不是「Proxy 比 defineProperty 好」,而是「Vue3 在能力、性能、兼容性之间怎么做的取舍」。

写在最后

你们线上被响应式坑过吗?我见过最典型的是 reactive 里放了一个第三方实例,结果每次改它的内部属性都触发视图更新,白烧性能。评论区聊聊你们踩过最刁钻的 Vue 响应式坑。

有用的话点个赞。

相关推荐
TiDi1 小时前
吸顶导航交互实现
前端
kisshyshy1 小时前
从Props透传到自定义Hook:系统梳理React跨层级通信与逻辑复用
前端·架构·代码规范
tiger8651 小时前
大语言模型面试和题解,梳理中国主流开源 LLM 系列的发展脉络、技术路线与工程取舍
人工智能·gpt·深度学习·算法·自然语言处理·面试·transformer
yume_sibai1 小时前
05-Flutter实战项目
前端·flutter
涛涛ing2 小时前
React 19.3 发布,但真正的赢家是 StyleX:当 AI 成为框架的“第一用户”
前端
修炼室2 小时前
海柔创新面试经验贴【HR面试】
面试
怕浪猫2 小时前
拆解 Google《AI Agent Handbook》:企业级 Agent 的六层架构与产品矩阵
面试·github·agent
tiger8652 小时前
大语言模型面试和题解,Context Engineering 怎么做?长 Prompt、动态上下文与 Memory 管理
人工智能·深度学习·算法·语言模型·自然语言处理·面试·nlp
zhangminghuan2 小时前
Vue vs React 实战拓展面试题|摒弃基础八股,聚焦业务开发核心难点(2026前端进阶)
前端