Vue 3 Composables 深度剖析:闭包陷阱与响应性丢失的底层逻辑
Vue 3 的组合式 API 极大地提升了逻辑复用的灵活性,Composables 成为了日常开发的核心范式。然而,Composables 本质上是基于 JavaScript 闭包与 Vue 响应式系统的深度结合。如果只停留在表面语法,极易陷入闭包陷阱与响应性丢失的深渊。
本文将结合 JS 底层原理,剖析 Composables 开发中的三大核心陷阱。
一、响应性丢失:解构与返回值的陷阱
在 Composable 中返回响应式数据时,错误的解构方式会切断 Proxy 的代理追踪,导致视图不更新。
反例:直接解构响应式对象
import { reactive } from 'vue';
function useUser() {
const user = reactive({ name: 'Alice', age: 25 });
return { ...user }; // 响应性彻底丢失!
}
const { name, age } = useUser();
// name 和 age 退化为普通字符串和数字,不再具备响应性
展开运算符 ... 只是在当前帧提取了值,返回的是一个普通对象的浅拷贝,与原 Proxy 断开了联系。
正例:使用 ref 或 toRefs 维持引用
import { reactive, toRefs, ref } from 'vue';
// 方案 1:toRefs 将每个属性转为 ref
function useUserGood() {
const user = reactive({ name: 'Alice', age: 25 });
return toRefs(user);
}
const { name, age } = useUserGood(); // name 和 age 均为 ref,保持响应性
// 方案 2:直接使用 ref(推荐基础类型)
function useUserBetter() {
const name = ref('Alice');
const age = ref(25);
return { name, age };
}
二、闭包陷阱:异步回调中的过时状态
Composable 中的异步函数经常会捕获旧的作用域快照,导致读取到过时的闭包变量。这是 JS 原生闭包机制与 Vue 响应式系统冲突的重灾区。
反例:异步函数捕获原始值快照
import { ref } from 'vue';
function useFetchData() {
const count = ref(0);
function badFetch() {
// 陷阱:将 ref.value 提前解构给普通变量
const currentCount = count.value;
setTimeout(() => {
console.log(currentCount); // 永远是创建时的快照值!
}, 1000);
}
return { count, badFetch };
}
闭包捕获的是变量 currentCount 的引用,而 currentCount 是一个原始值,赋值后即与 count.value 断开联系。
正例:在异步回调中实时读取 ref 或使用 watchEffect
import { ref, watchEffect } from 'vue';
function useFetchDataFixed() {
const count = ref(0);
// 方案1:始终在回调内部读取 .value(利用对象的引用不变性)
function safeFetch() {
setTimeout(() => {
console.log(count.value); // 每次执行都读取最新响应式状态
}, 1000);
}
// 方案2:利用 watchEffect 自动追踪依赖
watchEffect(() => {
const currentCount = count.value;
// 依赖变化时自动触发副作用,无需手动管理闭包
});
return { count, safeFetch };
}
三、内存泄漏:未清理的副作用与闭包引用
Composable 中注册的全局事件、定时器如果未在组件卸载时清理,由于闭包持有组件作用域的引用,将导致组件实例无法被 GC 回收。
反例:未清理副作用
import { ref, onMounted } from 'vue';
function useWindowResize() {
const width = ref(window.innerWidth);
const handler = () => {
width.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener('resize', handler);
});
// 组件卸载了,但 handler 闭包依然被 window 持有,width 也无法被回收
}
正例:利用 onScopeDispose 自动清理
Vue 3.2+ 引入了 effectScope API,任何 Composable 都应通过 onScopeDispose 注册清理逻辑,当所属组件卸载时,作用域会自动执行清理。
import { ref, onScopeDispose } from 'vue';
function useWindowResizeClean() {
const width = ref(window.innerWidth);
const handler = () => {
width.value = window.innerWidth;
};
window.addEventListener('resize', handler);
// 当前 effect scope 销毁时自动触发
onScopeDispose(() => {
window.removeEventListener('resize', handler);
});
return { width };
}
总结
编写高质量的 Vue 3 Composables,核心在于理清 JavaScript 闭包机制与 Vue 响应式代理的交互边界:
- 拒绝盲目解构:始终返回
ref或使用toRefs,避免在传递过程中切断 Proxy 追踪。 - 警惕异步快照:在异步回调中实时读取
.value,绝不在异步边界外提前解构原始值。 - 严格清理副作用:借助
onScopeDispose将闭包生命周期与组件/EffectScope 绑定,杜绝内存泄漏。
评论区
登录 后参与评论