customRef 与 ref
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准备好的内置ref
return {
keyword
}
}
}
</script>
展示效果:
第二步:定义自己的 ref 函数,并且使用它
setup(){
// let keyword = ref('hello') //使用Vue准备好的内置ref
// 定义自己的 ref 函数,接收值,并 return 出具体的值,否则返回undefined
function 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
传递时,可能会影响父组件和子组件之间的关系,
- 依赖追踪不完整
- 更新通知的不一致
- 异步逻辑导致的延迟