ref() 我们已经很熟悉了,就是用来定义响应式数据的,其底层原理还是通过 Object.defineprotpty 中的 get 实现收集依赖( trackRefValue 函数收集),通过 set 实现分发依赖通知更新( triggerRefValue 函数分发 )。我们看看 ref 的源码就知道了
class RefImpl {private _value: any; // 用来存储响应值private _rawValue: any; // 用来存储原始值public dep?: Dep = undefined; // 用来收集分发依赖public readonly __v_isRef = true; //是否只读,暂不考虑// 接收 new RefImpl() 传递过来的 rawValue 和 shallow constructor(value, public readonly __v_isShallow: boolean) {// 判断是否需要深层响应,如果不用,直接返回 Value 值,如果需要深层响应,则调用 toRaw 函数解除 value 的响应式,将其转化为原始值,以保证后续的深层响应this._rawValue = __v_isShallow ? value : toRaw(value);// 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应this._value = __v_isShallow ? value : reactive(value);}get value() {// 收集依赖trackRefValue(this);// 返回响应式数据return this._value;}set value(newVal) {// 将 newVal 转化为原始值,并于初始原始值比较,若不同,则准备更新数据,渲染页面,分发依赖if (hasChanged(toRaw(newVal), this._rawValue)) {//判断是否需要深层响应,如果不用,直接返回 newVal 值,如果需要深层响应,则调用 toRaw 函数解除 newVal 的响应式,将其转化为原始值,以保证后续的深层响应this._rawValue = this.__v_isShallow ? newVal : toRaw(newVal);// 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应this._value = this.__v_isShallow ? newVal : reactive(newVal);// 分发依赖,通知更新triggerRefValue(this);}}
}
具体的关于 ref 的使用以及更深层的理解请参考之前的文章 -- ref 函数
那么这个 customRef 函数是用来干啥的呢?
customRef
概念:创建一个自定义的 ref 函数,在其内部显式声明对其依赖追踪和更新触发的控制方式。
前面一句好理解,创建一个自定义的 ref ,其类型是一个函数,函数体内部的逻辑内容自定义。
后面一句就有点绕了,显式声明对其依赖追踪和更新触发的控制方式该怎么理解呢?
我们看看 ref 就知道了,当我们调用 ref 之后,读取数据时,Vue 底层就会自动去在 get 中收集依赖。修改数据时,会自动在 set 中分发依赖。这是不需要我们关心的,我们只需要调用 ref 函数就可以实现了。
但是 customRef 并没有按照 ref 的逻辑去实现,customRef 的处理是:既然你都自定义了,那你就自定义完整一点,依赖收集和分发工作你也自己做了,别去麻烦 Vue 底层在给你适配转化一次。
用法:customRef()
预期接收一个工厂函数作为参数,这个工厂函数接受 track
和 trigger
两个函数作为参数,并返回一个带有 get
和 set
方法的对象。
按照官网的例子我们一点点实现优化:创建一个自定义 ref ,实现防抖。具体效果就是我在 input 框中输入值,延时展示值。
第一步:不延迟,直接同步展示,v-model 双向绑定数据,插值语法展示数据,setup 定义数据
<template><input type="text" v-model="keyword"><h3>{{keyword}}</h3>
</template><script>import {ref} from 'vue'export default {name:'Demo',setup(){let keyword = ref('hello') //使用Vue准备好的内置refreturn {keyword}}}
</script>
展示效果:
第二步:定义自己的 ref 函数,并且使用它
setup(){// let keyword = ref('hello') //使用Vue准备好的内置ref// 定义自己的 ref 函数,接收值,并 return 出具体的值,否则返回undefinedfunction myRef(value) {console.log(value);return value}let keyword = myRef('hello') // 使用自定义的 ref 函数return {keyword}
}
此时我们发现,当我们使用自定义 ref 函数 时,因为我们并没有对这个数据进行响应式处理,所以页面数据并没有同步更新,这个时候我们就需要用到 customRef 来实现内部的逻辑。
第三步:调用 customRef 实现内部逻辑,按照 customRef() 的使用方法,完善 myRef()
这是 vscode 插件的提示语法,可以看到 customRef() 的完整用法。所以,我们完善一下 myRef()
function myRef(value) {return customRef((track,trigger) => {return {get() {// ...},set() {// ...}}})
}
到了这一步是不是就很眼熟了,这不就是 ref() 函数里面的响应式么,取值调 get,修改调 set。按照想法实现一下
function myRef(value) {return customRef((track,trigger) => {return {get() {return value},set(newValue) {value = newValue}}})
}
虽然数据发生了变更,但是页面并没有同步更新
这是因为数据只是发生了变更,但是并没有实现依赖追踪和触发更新,这个时候,我们在看看 ref() 的源码。
get value() {// 收集依赖trackRefValue(this);// 返回响应式数据return this._value;}set value(newVal) {// 判断逻辑 ......// 更新数据this._value = this.__v_isShallow ? newVal : reactive(newVal);// 分发依赖,通知更新triggerRefValue(this);}
在 ref() 中,在get 中收集依赖,在 set 中分发依赖,按这个模式,我们在 customRef() 中的 get 和 set 中也应该收集或分发。而 customRef 接收的工厂函数接收 track
和 trigger
两个函数作为参数,这两个函数其实就是对应的 ref() 中的 trackRefValue() 和 triggerRefValue() ,于是完善后的代码就成了这样。
function myRef(value) {return customRef((track,trigger) => {return {get() {track() // 先收集依赖,告诉Vue 这个 value 值是需要被追踪的return value // 然后返回被追踪的值,此时Vue底层已经对 value 实现了追踪},set(newValue) {value = newValue // 先设置值,因为 value 被追踪,所以数据改变时,Vue底层是能监听到trigger() // 然后分发依赖,告诉 Vue 需要更新界面}}})
}
实现的效果
到了这里,其实我们就完成了与第一步同样的效果:不延迟,直接同步展示。
剩下的就是实现防抖了。当数据改变时,我们通过 setTimeout 我们可以实现延迟 500ms 展示值。
set(newValue) {setTimeout(() => {value = newValue;trigger();}, 500);
},
但是我们发现,当过快输入时,值出现了诡异的变动,会突然卡一下,这是因为,每次改变数据时,都会开启一个定时器,但是定时器却并没有清除,这就导致累计了多个定时器才会出现这种情况。
按照标准防抖的流程,那就是在一定的时间内只执行一次,如果此时重复触发,则重新开始计时。代码改进之后展示
function myRef(value) {return customRef((track, trigger) => {let timer // 定义变量,接收定时器return {get() {track();return value;},set(newValue) {clearTimeout(timer); // 每次开启定时器之前先清除之前的定时器,防止出现错误timer = setTimeout(() => {value = newValue;trigger();}, 500);},};});
}
连续快速点击效果:只有在最后一次点击完成,且定时器延迟触发之后,才会展示改变后的值
慢速点击效果:每次点击都等待定时器执行完毕之后再触发下一次动作
到了这里,其实我们就完成了对依赖项跟踪和更新触发进行显式控制。可以看到,track()
应该在 get()
方法中调用,而 trigger()
应该在 set()
中调用。但是其实我们完全实控制了 track()、trigger() 的使用,包括但不限于在哪使用,是否需要使用等。
问题点
当你将 customRef
作为 prop
传递时,它可能会影响父组件和子组件之间的关系,尤其是在响应式系统的依赖追踪和更新通知方面。
案例代码
// 自定义 ref,没有调用 track()
function useCustomRef(value) {return customRef((track, trigger) => ({get() {track()return value;},set(newValue) {value = newValue;trigger(); // 触发更新}}));
}// 父组件
export default {setup() {const customValue = useCustomRef('Hello');return { customValue };},template: '<ChildComponent :propValue="customValue" />'
};// 子组件
export default {props: {propValue: {type: Object,required: true}},watch: {propValue(newValue) {console.log('Prop value updated:', newValue);}},template: '<div>{{ propValue }}</div>'
};
1. 依赖追踪不完整
在Vue 响应式系统中 ,Vue会自动进行依赖追踪。当父组件传递一个 ref
或响应式对象作为 prop
给子组件时,Vue 会追踪这个 prop
的依赖。
但是,customRef
可以自定义依赖追踪逻辑。如果你在 customRef
的 get
方法中没有正确调用 track()
,Vue 就无法知道子组件在依赖这个 prop
。这意味着,当父组件更新这个 prop
时,子组件可能无法感知到这个变化,因为依赖关系没有被正确建立。
2. 更新通知的不一致
当你在 customRef
的 set
方法中没有正确调用 trigger()
,即使 prop
在父组件中被更新,子组件也不会收到更新通知。这会导致子组件的数据与父组件不同步,从而产生 UI 不一致的问题。
3. 异步逻辑导致的延迟
如果 customRef
中包含异步逻辑(例如防抖或节流),这种延迟处理可能会导致子组件在接收 prop
时得到的是过时的数据。这在需要子组件立即响应父组件更新的场景下,可能引发状态不同步的问题。
在上面的例子中,debouncedRef
可能导致子组件在 prop
变更后并未立即更新,而是延迟更新,可能引发父子组件数据状态不同步的问题。
总结
1、customRef的作用:
创建一个自定义的 ref 函数,在其内部显式声明对其依赖追踪和更新触发的控制方式。
2、customRef 接收的工厂函数接收 track
和 trigger
两个函数作为参数,这两个函数其实就是对应的 ref() 中的 trackRefValue() 和 triggerRefValue() ,并返回一个带有 get
和 set
方法的对象。
3、一般来说,track()
应该在 get()
方法中调用,而 trigger()
应该在 set()
中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
4、当 customRef
作为 prop
传递时,可能会影响父组件和子组件之间的关系,
- 依赖追踪不完整
- 更新通知的不一致
- 异步逻辑导致的延迟