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 xobj.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,而是在对象外面套一层“代理”。
- 你对代理对象做任何操作(读、写、删、遍历、
in、Reflect.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 ap.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 更新)
常见误区(你可能正卡在这里)
- “customRef 的 get/set 和 Proxy 的 get/set 是不是一样?”
- 不一样。
Proxy 的 get/set 是“JS 层面的拦截机制”;customRef 的 get/set 是“Vue 响应式系统里的一套协议”。
- “defineProperty 也能写 get/set,那是不是就等价 Proxy?”
- 不等价。
defineProperty 是“属性级”,Proxy 是“对象级 + 更全的操作类型”。
- “为什么 Vue3 用 Proxy 还要有 customRef?”
- Proxy 解决的是“对象/数组的响应式拦截全面性”。
customRef 解决的是“更新策略”:比如防抖、节流、异步合并,这些不是 Proxy 本身能表达的。
什么是依赖,有什么用?
在 Vue 响应式里,“依赖(dependency)”指的不是“某个变量依赖另一个变量”,而是:
- 某段会自动重新运行的函数(组件渲染函数、
computed、watchEffect、watch的回调等) - 依赖了某个响应式数据的读取结果
- 所以当这个数据变化时,这段函数需要被重新执行
换句话说:依赖 = “订阅者列表”里的订阅者。
谁订阅?就是那些“读了响应式数据、并且需要在数据变更后重新跑”的函数。
什么是“副作用函数 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> // 依赖集合:一堆需要重跑的 effecttrack() => dep.add(activeEffect)“trigger”在干嘛?——通知依赖集合里的 effect 重跑(或排队重跑)
当你修改响应式数据时:
count.value = 1state.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 值(比如 msg 或 class)你希望“什么时候变化触发子/孙组件更新”,我可以用一个最小例子把“渲染 effect + track/trigger”的过程按步骤走一遍。
赞助支持
如果这篇文章对你有帮助,欢迎赞助支持!
部分内容可能已过时