2258 字
11 分钟

defineProperty和Proxy和customRef,依赖详解

欢迎来到我的博客!#

使用 Markdown 格式编写。

今天学到了什么#

  • defineProperty和Proxy和customRef中get和set的区别
  • 什么是依赖,有什么用?

defineProperty和Proxy和customRef中get和set的区别#

把这三个东西拆开来讲,你会更好理解:它们都出现了“get / set”字样,但“拦截的对象”和“触发更新的机制”完全不是一回事。


1)Object.defineProperty:给“某个对象的某个属性”装门禁

它做的事:在对象 obj 的某个属性 key 上,安装一个“属性描述符”,里面可以放 get() / set(v)

  • 你读 obj.key → 会走这个 get()
  • 你写 obj.key = v → 会走这个 set(v)
  • 只对这个属性生效(想拦截 100 个属性,就得装 100 次)

示例(核心感觉):

const obj = {};
let _x = 0;
Object.defineProperty(obj, 'x', {
get() {
console.log('get x');
return _x;
},
set(v) {
console.log('set x', v);
_x = v;
}
});
obj.x; // get x
obj.x = 10; // set x 10

它的“天生短板”(也是 Vue2 当年要打补丁的原因):

  • 只能拦截已存在并且你“装过门禁”的属性
    obj.newKey = 123 这种“新增属性”,默认不会被拦截(除非你再 defineProperty 一次)
  • 删除属性 delete obj.x、遍历 Object.keys(obj)in 操作符等,不像 Proxy 那么容易统一拦截
  • 数组下标、length 变化等场景很麻烦(可以做,但成本高、坑多)

一句话:defineProperty = “按属性装监控”


2)Proxy:给“整个对象”套一个代理壳

Proxy 的思路完全不同:不是在对象内部每个属性上装 getter/setter,而是在对象外面套一层“代理”。

  • 你对代理对象做任何操作(读、写、删、遍历、inReflect.ownKeys……)
  • 都会先进入对应的“trap(陷阱函数)”
  • Vue 3 的 reactive() 就是这个路线

示例:

const raw = { a: 1 };
const p = new Proxy(raw, {
get(target, key, receiver) {
console.log('proxy get', key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('proxy set', key, value);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log('proxy delete', key);
return Reflect.deleteProperty(target, key);
}
});
p.a; // proxy get a
p.b = 2; // proxy set b 2 (新增属性也能拦截)
delete p.a; // proxy delete a (删除也能拦截)

Proxy 的关键特点:

  • 能“天然覆盖”新增/删除/遍历/数组等更多操作(不仅仅是 get/set)
  • 但注意:只有对“代理对象 p”操作才会触发拦截
    你要是绕过代理去改原对象 raw.a = 2,Proxy 是感知不到的(Vue 3 也同理:要用响应式对象本身)

一句话:Proxy = “按对象整体代理”


3)customRef 的 get/set:不是 JS 拦截,而是 Vue 给你的“依赖收集/触发更新”钩子

customRef 是 Vue 提供的 API,用来创建一个“ref”,但让你自己决定:

  • 什么时候把当前组件/副作用函数记录为依赖(track()
  • 什么时候通知依赖更新(trigger()

它的 get() / set() 不是在拦截某个对象属性,也不是 Proxy trap;它只是定义 myRef.value 被读取/赋值时,Vue 内部该怎么“追踪/更新”。

典型用途:防抖 ref、节流 ref、按条件触发更新、异步合并更新等。

示例:防抖 ref(最经典):

import { customRef } from 'vue';
function debounceRef(initialValue, delay = 300) {
let value = initialValue;
let timer;
return customRef((track, trigger) => ({
get() {
track(); // 告诉 Vue:有人依赖了这个 value
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger(); // 告诉 Vue:现在才更新依赖
}, delay);
}
}));
}

这段代码里发生的事情是:

  • 组件渲染时读了 r.value → 会进 get()track() → 组件被记录为依赖
  • 你频繁 r.value = ... → 会进 set(),但你不立刻 trigger(),而是延迟触发
    所以界面不会每次都更新,只在 delay 后更新一次

一句话:customRef = “自定义 ref 的追踪/触发时机”


把差异用一句话对齐(最重要)

  • Object.defineProperty 的 get/set:拦截的是对象属性访问本身
  • Proxy 的 traps:拦截的是对整个对象的各种操作
  • customRef 的 get/set:拦截的是ref.value 的读写,并决定何时 track/trigger(影响 Vue 更新)

常见误区(你可能正卡在这里)

  1. “customRef 的 get/set 和 Proxy 的 get/set 是不是一样?”
  • 不一样。
    Proxy 的 get/set 是“JS 层面的拦截机制”;customRef 的 get/set 是“Vue 响应式系统里的一套协议”。
  1. “defineProperty 也能写 get/set,那是不是就等价 Proxy?”
  • 不等价。
    defineProperty 是“属性级”,Proxy 是“对象级 + 更全的操作类型”。
  1. “为什么 Vue3 用 Proxy 还要有 customRef?”
  • Proxy 解决的是“对象/数组的响应式拦截全面性”。
    customRef 解决的是“更新策略”:比如防抖、节流、异步合并,这些不是 Proxy 本身能表达的。

什么是依赖,有什么用?#

在 Vue 响应式里,“依赖(dependency)”指的不是“某个变量依赖另一个变量”,而是:

  • 某段会自动重新运行的函数(组件渲染函数、computedwatchEffectwatch 的回调等)
  • 依赖了某个响应式数据的读取结果
  • 所以当这个数据变化时,这段函数需要被重新执行

换句话说:依赖 = “订阅者列表”里的订阅者
谁订阅?就是那些“读了响应式数据、并且需要在数据变更后重新跑”的函数。

什么是“副作用函数 effect”?#

Vue 内部把这些“需要自动重跑的函数”统称为 effect(副作用):

  • 组件渲染:渲染函数会读 state.xxx / ref.value,读完要能自动更新 DOM
  • computed:getter 读了响应式数据,相关数据变了它要重新计算
  • watchEffect:回调里读了什么,就自动监听什么,变化就重跑
  • watch(source, cb):source 读了什么,变化就触发 cb

你可以用 watchEffect 直观看到 “effect 会重跑”:

import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
console.log('effect run, count =', count.value)
})
count.value++ // 会让上面的 watchEffect 再跑一次

这里的 watchEffect 回调 就是一个 effect。


“track”在干嘛?——把 effect 记到“依赖集合”里#

当一个 effect 正在执行时(比如组件正在渲染、watchEffect 正在跑),它会去读取响应式数据:

  • 读取 count.value
  • 读取 state.name

Vue 会在“读取(get)”发生时做一件事:把“当前正在运行的 effect”记录到这个数据的依赖集合里。这个动作就叫 track()

你可以把它想成:

  • 当前正在运行的 effect = “我是谁”(例如:组件渲染 effect)
  • 当前读到的数据 = “我读了谁”(例如:count.value
  • track() = “把我登记到它的订阅列表里”

所以 track() 的本质是:建立订阅关系:数据 → effects(订阅者集合)

一个很简化的内部结构可以理解成这样(伪代码):

dep = Set<effect> // 依赖集合:一堆需要重跑的 effect
track() => dep.add(activeEffect)

“trigger”在干嘛?——通知依赖集合里的 effect 重跑(或排队重跑)#

当你修改响应式数据时:

  • count.value = 1
  • state.name = 'x'

Vue 会在“写入(set)”发生时做一件事:把这个数据对应的依赖集合 dep 里所有 effect 找出来,让它们重新执行(或加入调度队列)。这个动作就叫 trigger()

还是类比订阅:

  • 你修改了数据 = “发布了一次更新”
  • trigger() = “通知所有订阅者:你们该更新了”

伪代码理解:

trigger() => dep.forEach(effect => effect.runOrSchedule())

依赖有什么用?——它是“精准更新”的关键#

如果没有依赖收集(track/trigger),Vue 要更新页面就只能:

  • 要么每次任何数据变动都全量重渲染所有组件(太慢)
  • 要么你手动写一堆监听逻辑(太累、容易漏)

有了依赖收集后,Vue 可以做到:

  • 组件渲染时读了 count,就只订阅 count
  • 以后只有 count 变了才让这个组件重渲染
  • 没读过的状态变了,不会影响它

这就是响应式的核心价值:自动 + 精准


为什么 customRef 要你手动 track/trigger?#

普通 ref 是 Vue 内部已经写好了:

  • get value 里自动 track()
  • set value 里自动 trigger()

customRef 的意义是:把“什么时候 track / 什么时候 trigger”交给你控制

例如防抖 ref:

  • get():照常 track()(否则组件读它不会建立依赖,后续也就不会更新)
  • set():不要立刻 trigger(),等防抖时间到了再 trigger()
    => 达到“输入很频繁但界面不频繁更新”的效果

一句话总结(抓住主线)#

  • 依赖:哪些 effect(渲染/计算/监听)需要在数据变更后重跑
  • track():在 effect 运行期间读数据时,把 effect 登记为这个数据的订阅者
  • trigger():写数据时,通知所有订阅者 effect 重新运行(或排队运行)

如果你想把它和你正在学的 $attrs 页面串起来:你可以告诉我父组件传给子组件的某个 $attrs 值(比如 msgclass)你希望“什么时候变化触发子/孙组件更新”,我可以用一个最小例子把“渲染 effect + track/trigger”的过程按步骤走一遍。

赞助支持

如果这篇文章对你有帮助,欢迎赞助支持!

赞助
defineProperty和Proxy和customRef,依赖详解
https://firefly.cuteleaf.cn/posts/study-everyday/2026-05-10今日学习-defineproperty和proxy和customref/
作者
LJC
发布于
2026-05-10
许可协议
CC BY-NC-SA 4.0
最后更新于 2026-05-10,距今已过 114 天

部分内容可能已过时

目录