前端开发··1 阅读·预计 8 分钟

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 断开了联系。

正例:使用 reftoRefs 维持引用

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 响应式代理的交互边界:

  1. 拒绝盲目解构:始终返回 ref 或使用 toRefs,避免在传递过程中切断 Proxy 追踪。
  2. 警惕异步快照:在异步回调中实时读取 .value,绝不在异步边界外提前解构原始值。
  3. 严格清理副作用:借助 onScopeDispose 将闭包生命周期与组件/EffectScope 绑定,杜绝内存泄漏。
0 评论

评论区

登录 后参与评论