系列文章目录
Vue3 组合式 API 进阶:深入解析 customRef 的设计哲学与实战技巧
Vue3 watchEffect 进阶使用指南:这些特性你可能不知道
Vue3高级特性:深入理解effectScope及其应用场景
文章目录
一、前言
在 Vue 3 的组合式 API 中,customRef 是一个强大但不太为人熟知的特性。它允许开发者自定义 ref 的行为,包括控制依赖追踪和更新触发。本文将带您深入了解 customRef 的核心概念、使用场景和实际应用,帮助你充分利用这一特性提升应用性能。
二、什么是 customRef?
customRef 是 Vue 3 提供的一个高级 API,它允许开发者创建自定义的响应式引用对象。与标准 ref 不同,customRef 让您可以完全控制依赖追踪和触发更新的时机。这对于需要控制依赖追踪的时机或需要自定义值变化时的行为(如防抖、节流)的开发场景中特别有用。
何时使用 customRef?
在以下场景中,customRef 特别有用:
-
需要控制依赖追踪的时机
-
需要自定义值变化时的行为(如防抖、节流)
-
需要与外部系统(如 localStorage、IndexedDB)集成
-
需要实现复杂的状态管理逻辑
-
需要创建带有历史记录或撤销/重做功能的状态
三、customRef API 详解
API 基本结构
import { customRef } from 'vue';
const myCustomRef = customRef((track, trigger) => {
return {
get() {
// 追踪依赖
track();
//return 返回值
},
set(newValue) {
// 这里写更新值逻辑
// 触发更新
trigger();
}
};
});
语法说明:
它接受一个工厂函数作为参数,该工厂函数返回一个包含 get 和 set 方法的对象。通过myCustomRef.value访问属性值将调用内部get方法,通过myCustomRef.value=xxx赋值将调用内部set方法。
工厂函数参数解析:
- track():调用此方法会通知 Vue 追踪当前值的依赖关系
- trigger():调用此方法会通知 Vue 触发依赖更新。
返回值
customRef 返回一个自定义的 ref 对象,该对象具有普通 ref 的所有特性,但行为由你定义。
快速上手示例代码——自带防抖ref:
import { customRef } from 'vue'
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track() // 追踪依赖
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 触发更新
}, delay)
}
}
})
}
三、实际开发场景及示例
1.防抖
防抖(Debounce)是一种常见的优化技术,用于限制函数的执行频率,确保在特定时间内只有最后一次调用生效。这在处理高频触发事件(如输入框搜索、窗口大小变化等)时特别有用,可以有效减少性能开销
封装一个通用带防抖功能customRef
debouncedRef.js
import { customRef } from "vue";
/**
*
* @param {*} value :值
* @param {*} delay :防抖延迟时间
* @param {*} immediate :是否立即执行
* @returns customRef
*/
export function useDebounceRef(value, delay = 200, immediate = false) {
let timeout = null;
let initial = true;
return customRef((track, trigger) => {
return {
get() {
track(); // 追踪依赖
return value;
},
set(newValue) {
if (initial && immediate) {//立即执行
initial = false;
value = newValue;
trigger();// 触发更新
return;
}
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger(); // 触发更新
}, delay);
},
};
});
}
场景1: 防抖搜索输入框
在搜索场景中,频繁触发搜索请求会带来性能问题。使用 customRef 可以轻松实现防抖功能:
<template>
<div class="container">
<input v-model="keyword" placeholder="关键词搜索" />
<ul>
<li v-for="(item, index) in list" :key="item.id">{{ item.title }}</li>
</ul>
</div>
</template>
<script setup>
import { useDebounceRef } from "./hooks/debounceRef.js";
import {ref, watch } from "vue";
//输入关键词
const keyword = useDebounceRef("", 300); //默认值为空,防抖时间为300ms
//文章列表
const list = ref([]);
watch(keyword, (newValue, oldValue) => {
//模拟接口请求,搜索文章数据
setTimeout(() => {
list.value = new Array(10).fill().map((item, index) => {
return {
id:index,
title: `文章${index + 1}:${newValue}`,
};
});
}, 200);
});
</script>
运行效果:
场景2:窗口大小调整处理
在一些需要监听窗口大小变化并执行计算或重新渲染UI的场景中,不希望频繁触发重计算/重绘。例如响应式布局(echart图表、海康视频插件播放窗等)根据窗口尺寸变化自适应大小
<template>
<div class="container">
<ChildComponent :width="windowWidth" />
</div>
</template>
<script setup>
import { useDebounceRef } from "./hooks/debounceRef.js";
import ChildComponent from "./ChildComponent.vue"; //子组件
//窗口大小
const windowWidth = useDebounceRef(window.innerWidth);
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
</script>
场景3: 表单输入验证
用户输入表单字段时,实时验证但不希望过于频繁触发验证逻辑。
<template>
<div class="container">
<form @submit.prevent="submitForm">
<div class="form-group">
<label>Email:</label>
<input
v-model="email"
type="email"
/>
<div v-if="emailError" class="error-message">{{ emailError }}</div>
</div>
<button type="submit">Submit</button>
</form>
</div>
</template>
<script setup>
import { useDebounceRef } from "./hooks/debounceRef.js";
import {ref,watch} from 'vue';
//输入邮箱
const email = ref('')
// 对原始 email 进行防抖处理
const debouncedEmail = useDebounceRef(email.value, 500)
const emailError = ref('')
// 监听防抖后的邮箱变化,进行验证
watch(debouncedEmail, (newValue) => {
if (!newValue) {
emailError.value = '邮箱必填'
} else if (!/\S+@\S+\.\S+/.test(newValue)) {
emailError.value = '格式错误'
} else {
emailError.value = ''
}
})
// 同步原始值到防抖ref
watch(email, (newValue) => {
debouncedEmail.value = newValue
})
const submitForm = () => {
if (!emailError.value) {
// 提交表单逻辑
console.log('提交邮箱为:', email.value)
}
}
</script>
运行效果:
场景4:优化实时图表(echart等)数据更新
当实时数据高频更新时,避免图表频繁重绘。
<template>
<div class="container">
<ChartComponent :data="debouncedChartData" />
</div>
</template>
<script setup>
import { ref, onMounted ,onUnmounted} from 'vue'
import { useDebouncedRef } from './useDebouncedRef'
const rawChartData = ref([])
// 对图表数据进行防抖处理,延迟 500ms
const debouncedChartData = useDebouncedRef(rawChartData.value, 500)
// 模拟实时数据更新(每秒更新 10 次)
onMounted(() => {
const interval = setInterval(() => {
rawChartData.value = [...rawChartData.value, Math.random()]
// 更新防抖 ref
debouncedChartData.value = rawChartData.value
}, 100)
onUnmounted(() => clearInterval(interval))
})
</script>
总结
customRef 实现的防抖在实际开发中的核心价值在于:
- 减少不必要的计算或 API 请求,提升性能。
- 保持数据响应式的同时控制更新频率。
- 统一处理防抖逻辑,避免在组件中重复实现。
使用时需注意:
- 根据需求可以将原始数据与防抖数据分离管理(如示例 3 中的 email 和 debouncedEmail)。
- 在组件卸载时清理定时器,防止内存泄漏。
- 根据场景调整防抖延迟时间(通常 200-500ms 适合)。
2.节流
在实际开发中,customRef 实现的节流(Throttle)功能常用于需要限制操作频率的场景,例如滚动加载、高频点击事件、实时数据更新等
封装一个通用带节流功能customRef
throttleRef.js
import { customRef } from "vue";
/**
*
* @param {*} value :值
* @param {*} delay :节流延迟时间
* @returns customRef
*/
export function useThrottleRef(value, delay = 200) {
let timeout = null;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
if (!timeout) {
value = newValue;
trigger(); // 触发更新
timeout = setTimeout(() => {
timeout = null;
},delay);
}
},
};
});
}
场景1:列表滚动触底加载更多数据
监听滚动事件加载更多内容,但避免频繁触发网络请求。
<template>
<div class="list-container" ref="listRef">
<div v-for="item in items" :key="item" class="item">{{ item }}</div>
<div v-if="loading" class="loading">加载更多...</div>
</div>
</template>
<script setup>
import { useThrottleRef } from "./hooks/throttleRef.js";
import { ref, watch, onMounted, onBeforeUnmount} from "vue";
const listRef = ref(null);
const items = ref([1, 2, 3, 4, 5, 6, 7, 8,]);
const loading = ref(false);
const page = ref(1);
// 使用节流 ref 监听滚动位置
const scrollY = useThrottleRef(0, 200);
//滚动监听
const handleScroll = () => {
if (!listRef.value) return;
const { scrollTop, scrollHeight, clientHeight } = listRef.value;
// 距离底部 100px 时触发加载
if (scrollTop + clientHeight >= scrollHeight - 100) {
scrollY.value = scrollTop; // 触节流更新,触发 loadMore 监听
}
};
onMounted(() => {
listRef.value && listRef.value.addEventListener("scroll", handleScroll);
});
onBeforeUnmount(() => {
listRef.value && listRef.value.removeEventListener("scroll", handleScroll);
});
//加载更多数据
const loadMore = async () => {
if (loading.value) return;
loading.value = true;
page.value++;
// 模拟加载更多数据
await new Promise((resolve) => setTimeout(resolve, 800));
// 追加新数据
const newItems = Array(5)
.fill(0)
.map((v, i) => items.value.length + i + 1);
items.value = [...items.value, ...newItems];
loading.value = false;
};
// 监听节流后的滚动位置变化
watch(scrollY, loadMore);
</script>
运行效果:
场景2:高频点击事件防重触发
防止按钮在短时间内被多次点击,避免重复提交表单或触发操作。
对throttleRef.js进行改进:
import { customRef } from "vue";
/**
*
* @param {*} value :值或函数
* @param {*} delay :节流延迟时间
* @returns customRef
*/
export function useThrottleRef(value, delay = 200) {
let timeout = null;
//是否函数类型
let isFunction = typeof value === "function";
return customRef((track, trigger) => {
let throttledFunc = null;//节流函数
if (isFunction) {
throttledFunc = (...args) => {
if (!timeout) {
value(...args);
trigger();
timeout = setTimeout(() => {
timeout = null;
}, delay);
}
};
}
//函数类型
if (isFunction) {
return {
get() {
track();
return throttledFunc;
},
set() {}, // 函数无需设置值
};
} else {
//普通值类型
return {
get() {
track();
return value;
},
set(newValue) {
if (!timeout) {
value = newValue;
trigger(); // 触发更新
timeout = setTimeout(() => {
timeout = null;
}, delay);
}
},
};
}
});
}
使得入参既可以是函数,也可以是普通值
<template>
<div class="button-demo">
<button @click="throttledClick" :disabled="isLoading">
{{ isLoading ? "提交中..." : "点击提交" }}
</button>
<p>已提交次数: {{ clickCount }}</p>
</div>
</template>
<script setup>
import { ref} from "vue";
import { useThrottleRef } from "./hooks/throttleRef.js";
const clickCount = ref(0);
const isLoading = ref(false);
// 使用节流 ref 控制点击频率
const throttledClick = useThrottleRef(() => {
//提交数据
isLoading.value = true;
clickCount.value++;
// 模拟接口请求
setTimeout(() => {
isLoading.value = false;
console.log("提交次数:", clickCount.value);
}, 2000);
}, 2000); // 2000ms 内只能点击一次
</script>
运行结果:
总结
customRef 实现的节流在实际开发中的核心价值在于:
- 限制高频操作的执行频率,避免浏览器卡顿或资源浪费。
- 保持数据响应式的同时控制更新节奏,平衡性能与用户体验。
- 统一处理节流逻辑,减少组件内的重复代码。
使用时需注意:
- 根据场景调整节流间隔(如滚动加载适合 200-300ms,实时数据适合 500-1000ms)。
- 处理边界情况(如首次加载、数据为空)。
- 确保在组件卸载时清理定时器或事件监听,避免内存泄漏
3.数据自动持久化
将自定义 ref 与 localStorage集成,实现数据(变量)自动持久化存储本地,也可以用在vuex或pina等第三方库上实现数据持久化。
localStorageRef.js
import { customRef } from "vue";
/**
*
* @param {*} key :localStorage的key
* @param {*} initialValue :默认值
* @returns
*/
export function useLocalStorageRef(key, initialValue) {
// 从 localStorage 获取值
let value = localStorage.getItem(key) ?? initialValue;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
value = newValue;
localStorage.setItem(key, newValue);
trigger();
},
};
});
}
页面使用:
<template>
<div class="container">
<input v-model="account" placeholder="请输入账号" />
<p>本地存储数据为: {{ account }}</p>
<button @click="handleReload">刷新页面</button>
</div>
</template>
<script setup>
import { useLocalStorageRef } from './localStorageRef'
const account = useLocalStorageRef('account', '')
const handleReload = () => {
window.location.reload()
}
</script>
运行效果:
4.实现只读 ref
创建一个只读的 ref,外部无法修改其值,保护敏感数据不被组件内部意外修改。
readonlyRef.js
import { customRef } from 'vue'
export function useReadonlyRef(value) {
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set() {
// 抛出警告
console.warn('readonly ref')
}
}
})
}
页面使用
<template>
<div class="container">
<button @click="increment">+1</button>
<p>Readonly: {{ readonlyCount }}</p>
</div>
</template>
<script setup>
import { useReadonlyRef } from './readonlyRef'
const readonlyCount = useReadonlyRef(0)
const increment = () => {
readonlyCount.value++
}
</script>
运行效果:
5. 带历史记录的 ref(撤销 / 重做功能)
记录值的变更历史,支持撤销和重做操作
historyRef.js
import { customRef, ref, computed, reactive } from "vue";
/**
*
* @param {*} initialValue :初始值
* @param {*} maxHistory :最大历史记录数
* @returns
*/
export function useHistoryRef(initialValue, maxHistory = 10) {
const history = ref([initialValue]); // 历史记录数组
const historyIndex = ref(0); // 当前历史索引
// 创建自定义customRef
const mainRef = customRef((track, trigger) => {
return {
get() {
track();
return history.value[historyIndex.value];
},
set(newValue) {
// 移除当前索引之后的历史记录
if (historyIndex.value < history.value.length - 1) {
history.value = history.value.slice(0, historyIndex.value + 1);
}
// 添加新值到历史记录
const newHistory = [...history.value, newValue];
// 限制历史记录长度
if (newHistory.length > maxHistory) {
newHistory.shift();
historyIndex.value = Math.max(0, historyIndex.value - 1);
}
history.value = newHistory;
historyIndex.value = newHistory.length - 1;
trigger(); // 触发响应式更新
},
};
});
// 撤销操作
const undo = () => {
if (historyIndex.value > 0) {
historyIndex.value--;
}
};
// 重做操作
const redo = () => {
if (historyIndex.value < history.value.length - 1) {
historyIndex.value++;
}
};
//是否可以撤销
const canUndo = computed(() => historyIndex.value > 0);
//是否可以重做
const canRedo = computed(() => historyIndex.value < history.value.length - 1);
// 使用 reactive 确保所有属性都是响应式的
return reactive({
value: mainRef,
undo,
redo,
canUndo,
canRedo,
history: computed(() => history.value),
historyIndex: computed(() => historyIndex.value),
historyLength: computed(() => history.value.length),
get current() {
return mainRef.value;
},
set current(newValue) {
mainRef.value = newValue;
},
});
}
页面使用:
<template>
<div class="container">
<!-- 直接绑定到 自定义ref 注意需要.value访问 -->
<input v-model="text.value" />
<!-- 使用方法 -->
<button @click="text.undo" :disabled="!text.canUndo">撤销</button>
<button @click="text.redo" :disabled="!text.canRedo">重做</button>
<!-- 访问附加属性 -->
<div>可撤销: {{ text.canUndo }}</div>
<div>可重做: {{ text.canRedo }}</div>
<div>历史记录长度: {{ text.historyLength }}</div>
<div>当前索引: {{ text.historyIndex }}</div>
</div>
</template>
<script setup>
import { useHistoryRef } from "./historyRef.js";
const text = useHistoryRef("");
</script>
运行效果:
四、使用技巧与注意事项
1. 避免过度使用
customRef 是一个底层 API,大多数情况下普通的 ref 和 computed 已经足够。只有在需要精确控制依赖追踪和更新触发时才使用。
2. 性能优化
通过合理使用 customRef,可以减少不必要的重新渲染,提高应用性能。但要注意,过度复杂的自定义逻辑可能会适得其反。
3. 与其他 API 结合
customRef 可以与 computed、watch 等 API 结合使用,构建更复杂的响应式逻辑。
总结
customRef 为 Vue 开发者提供了一种灵活且强大的方式来自定义响应式行为。通过掌握其 API 和应用场景,你可以优雅地处理高频事件、数据持久化、历史操作记录等创建更高效的 Vue 应用。在实际开发中,合理使用 customRef 能够帮助你解决许多性能和集成难题,是 Vue 3 工具箱中不可或缺的一部分。