Appearance
结论先行(TL;DR)
- 输入统一:接受
MaybeRefOrGetter<T>(值 /ref/computed/ getter 函数均可),在内部用toValue/unref标准化;这样既支持响应式参数,也不拒绝原始值。 - 衍生数据一律用
computed;副作用一律用watch/watchEffect;只在需要副作用时监听,避免滥用watch。 - 返回值一律是只读
ref或对象的readonly包装,外部不直接修改内部状态。 - 来自
props的参数,调用方用toRef(props, 'x')传入,而不是原始解构值;可避免"值丢失响应性"。 - 可选项(options)类参数:支持"静态值 + 响应式覆盖"双形态,内部通过
toValue读取,必要时对关键项watch。 - 性能:默认
shallowRef保存大对象;watch谨慎开启深度;对频繁变动源使用节流/防抖;flush: 'post'处理 DOM 相关副作用。 - 边界:组合多个源用
watch([a,b]);异步源配合watchEffect+ 取消;在onScopeDispose清理。
设计目标
- 调用体验一致:同一个 composable 既可接收原始值(一次性),也能接收响应式值(持续更新),调用成本最低。
- 内部可维护:对外暴露最小可变面,逻辑分层清晰(衍生 vs 副作用)。
- 性能稳:避免不必要的深度侦听与重算,易于控制更新频率。
标准输入类型与约定
- 统一签名:
input: MaybeRefOrGetter<T>- 调用方可传:
42、ref(42)、computed(...)、() => props.x。 - 组合式内部用
toValue(input)读取当前值,用watch(input, ...)订阅变化。
- 调用方可传:
- 对象/配置:
options?: MaybeRefOrGetter<Partial<Options>>,内部按需对关键字段加watch。 - 来自 props:推荐调用方先
toRef(props, 'x')再传入,避免响应性丢失。
何时用响应式输入 vs 原始值?
- 业务参数(会变):传响应式(
ref/computed/getter),Composable 自动跟随变化。 - 一次性初始化(不变):可传原始值(如常量开关/默认阈值)。
- 可变但低频:仍建议响应式;或传 getter(外部自行控制何时变化),内部只
toValue读取。
大厂常见做法:对所有可变入参一律允许响应式,但不强制,以降低接入门槛。
衍生与副作用的分工
- 衍生数据 →
computed- 纯函数式、可缓存、易测试。
- 副作用 →
watch/watchEffect- 需要观察具体源时用
watch(source, cb, options); - 依赖自动收集(可能多个源)时用
watchEffect,并正确处理清理函数。
- 需要观察具体源时用
返回值规范
- 返回
readonly(ref)或只读对象:防止外部误改内部状态。 - 若需对外暴露"命令式"改变,提供方法而非直接暴露可写
ref。
代码模板(生产级骨架)
说明:以下仅给出关键片段(符合你的"只给必要代码"的偏好)。复杂逻辑前加中文注释。
类型定义
ts
// types.ts
export type MaybeRefOrGetter<T> =
| T
| (() => T)
| import("vue").Ref<T>
| import("vue").ComputedRef<T>;Composable 实现
ts
// useSmartThing.ts
import {
ref,
shallowRef,
computed,
watch,
watchEffect,
toValue,
readonly,
onScopeDispose,
} from "vue";
import type { MaybeRefOrGetter } from "./types";
interface UseSmartThingOptions {
// 复杂逻辑:影响刷新频率与重算窗口
// 这里的 throttleMs 既可传原始值,也可传 ref/getter
throttleMs?: number;
deep?: boolean;
}
export function useSmartThing<T>(
input: MaybeRefOrGetter<T>,
options?: MaybeRefOrGetter<UseSmartThingOptions>
) {
// 复杂逻辑:内部状态大型对象用 shallowRef 降低不必要的深度依赖
const state = shallowRef<{ value: T; stamp: number }>({
value: toValue(input),
stamp: Date.now(),
});
const opts = () => ({ throttleMs: 0, deep: false, ...toValue(options) });
// 复杂逻辑:衍生值只用 computed,避免副作用
const pretty = computed(() => {
const v = state.value.value as unknown as T;
return String(v);
});
let timer: number | null = null;
// 复杂逻辑:副作用监听输入变化 + 节流控制
watch(
input,
(val) => {
const { throttleMs } = opts();
if (throttleMs && throttleMs > 0) {
if (timer) return;
// 复杂逻辑:节流窗口内只记录第一次
state.value = { value: val, stamp: Date.now() };
timer = window.setTimeout(() => {
timer = null;
}, throttleMs) as unknown as number;
} else {
state.value = { value: val, stamp: Date.now() };
}
},
{ immediate: true, deep: opts().deep, flush: "post" }
);
// 复杂逻辑:当 options 自身是响应式时,关键字段变化需要触发重应用(例如 deep/节流策略变化)
watch(
() => toValue(options),
() => {
// 这里按需重建监听/重算策略。简单起见仅触发一次性刷新。
state.value = { value: toValue(input), stamp: Date.now() };
},
{ deep: true }
);
onScopeDispose(() => {
if (timer) {
clearTimeout(timer);
timer = null;
}
});
return {
value: readonly(state), // 对外只读
pretty, // 衍生值
// 提供命令式 API(如需要)
// reset() { ... }
};
}"原始值如何确保更新?"的三种稳妥做法
核心思想:Composable 不强制输入必须是响应式,但内部要能"感知变化来源"。
1. 由调用方负责响应化(推荐)
- 来自
props:const xRef = toRef(props, 'x')后传入。 - 外部变量:
const xRef = ref(x)并在外部更新xRef.value。 - 优点:数据流最清晰,职责边界最明确(大厂偏好)。
2. Composable 接受 getter(函数)
- 传
() => props.x或() => someStore.value,内部watch(input, ...)可订阅。 - 适合:不方便造
ref的场景。
3. 显式刷新方法(兜底)
- 返回
refresh(),让调用方在原始值变化时手动触发。 - 用于:变化极少、接入成本敏感的工具类 composable。
典型调用方写法(防止响应性丢失)
ts
// 来自 props 的值,避免直接解构丢失响应性
const count = toRef(props, "count");
const { value, pretty } = useSmartThing(count, { throttleMs: 200 });
// 原始常量,也可以直接传(一次性)
const { value: once } = useSmartThing(123);
// 不便创建 ref 的场景,传 getter
const { value: viaGetter } = useSmartThing(() => route.query.id);监听策略与性能建议
- 尽量不用
deep: true;需要时对具体字段创建watch(() => obj.key, ...)。 - 大对象用
shallowRef,配合整体替换(immutable 写法)触发更新。 - 高频源(输入框、滚动位置)使用防抖/节流;
flush: 'post'处理布局/DOM 相关副作用。 - 拆分副作用:一个
watch只做一件事,方便定位与中止。 - 组合多源:
watch([a, b], ([na, nb], [oa, ob]) => { ... }),避免多重监听交叉触发。
返回 API 的一致性与可测试性
- 只读输出 + 命令式方法(而非暴露可写 ref)有利于单测与边界控制。
- 错误边界:在 composable 内部进行参数校验(类型/范围),尽早失败,日志可控。
- SSR:涉及
window/DOM 的副作用加环境判断或延迟到onMounted/flush: 'post'。
常见坑位与规避
- 直接解构 props 导致"失去响应性":总是用
toRef(props, 'k')/toRefs(props)。 - 滥用
watchEffect:当依赖集不稳定时会频繁触发;已知源优先watch(source, ...)。 - 深度监听大对象:高成本;改为分字段监听或结构化拷贝 +
shallowRef。 - 返回可写 ref 被外部改坏状态:一律
readonly封装。 - 没有清理副作用:使用
onScopeDispose,或在watch回调中返回清理函数。
小结:大厂风格的"可插可拔"组合式设计
- 输入宽容(值/
ref/getter 皆可)+ 内部标准化(toValue)。 - 衍生 =
computed,副作用 =watch,边界清晰。 - 返回只读 + 命令式方法,利于治理与测试。
- 性能可控:默认浅引用、精确监听、必要时节流防抖。
- 调用侧最佳实践:
toRef(props, 'x')、必要时传 getter,实在不便再用refresh()兜底。
以上模式在中大型项目验证稳定,既保证了接入友好,又维持了内部约束与性能。如果你有具体的 composable 正在写,我可以按这个规范帮你把签名与实现"落标"。