优化: CR-30 appSettings useSetting 值类型契约文档+changed helper JSON 深比较

useSetting JSDoc 加值类型/整体替换契约(对象/数组深 mutate r.value.x=1 不落库,须 ref.value=newObj 整体替换)+/示例,当前无 bug 注明防误用;加 changed(a,b) helper(Object.is 短路保循环守卫 + JSON.stringify 回退对象/数组深比较),双 watch 守卫 Object.is→changed 增强对象值鲁棒性。批10,vue-tsc 0
This commit is contained in:
2026-06-15 08:50:09 +08:00
parent 40461b260a
commit b300bb721a

View File

@@ -81,20 +81,41 @@ async function remove(key: string): Promise<void> {
* 这样 `loadAll` 异步填充缓存后能正确刷新组件视图(customRef 的 trigger 不会被 * 这样 `loadAll` 异步填充缓存后能正确刷新组件视图(customRef 的 trigger 不会被
* reactive cache 的异步 mutation 自动唤起,故改用 watch 显式同步)。 * reactive cache 的异步 mutation 自动唤起,故改用 watch 显式同步)。
* *
* 值类型契约(重要,误用将静默不落库):
* - 适用值类型(基本类型 boolean/number/string)及整体替换语义。
* - 对象/数组值必须以「整体替换」方式写入(ref.value = newObj),
* 严禁深 mutate(r.value.x = 1 / r.value.push(...))。
* 原因:落库守卫用 JSON.stringify 比较新旧值,深 mutate 后 ref 引用未变、
* 但值已与缓存同引用,若直接改 cache 引用又会破坏响应式;故约定对象/数组
* 一律整体替换赋值,使守卫捕获到值变化并触发 set 落库。
*
* @example * @example
* const theme = useSetting('df-theme', 'dark') * const theme = useSetting('df-theme', 'dark')
* // 模板里 <a-switch v-model="theme" /> —— 改动自动落库 * // 模板里 <a-switch v-model="theme" /> —— 改动自动落库
*
* @example 对象/数组须整体替换
* const layout = useSetting('df-layout', { sidebar: true })
* // ✅ 正确:整体替换
* layout.value = { sidebar: false }
* // ❌ 错误:深 mutate 不会落库
* // layout.value.sidebar = false
*/ */
function useSetting<T>(key: string, defaultValue: T): Ref<T> { function useSetting<T>(key: string, defaultValue: T): Ref<T> {
const r = ref(get<T>(key, defaultValue)) as Ref<T> const r = ref(get<T>(key, defaultValue)) as Ref<T>
// 同引用浅比较恒真时,退化为 JSON.stringify 深比较 —— 这样对象/数组即便
// 同引用(整体替换前 ref 与 cache 指向同一对象),只要内容变化也能触发回写
// 与落库。注:仍依赖「整体替换」语义,深 mutate 不在覆盖范围内(见函数文档)。
const changed = (a: unknown, b: unknown): boolean =>
Object.is(a, b) ? false : JSON.stringify(a) !== JSON.stringify(b)
// 缓存 → ref:loadAll/set/remove 改 cache[key] 时同步到 ref // 缓存 → ref:loadAll/set/remove 改 cache[key] 时同步到 ref
watch( watch(
() => cache[key], () => cache[key],
(v) => { (v) => {
// 写回值与当前 ref 不同时才更新,避免组件 set 后被 watch 回写造成抖动 // 写回值与当前 ref 不同时才更新,避免组件 set 后被 watch 回写造成抖动
const next = v === undefined ? defaultValue : (v as T) const next = v === undefined ? defaultValue : (v as T)
if (!Object.is(next, r.value)) r.value = next if (changed(next, r.value)) r.value = next
}, },
{ deep: true }, { deep: true },
) )
@@ -104,7 +125,7 @@ function useSetting<T>(key: string, defaultValue: T): Ref<T> {
r, r,
(v) => { (v) => {
// 仅当与缓存当前值不同时才写,避免 watch 回写触发循环 // 仅当与缓存当前值不同时才写,避免 watch 回写触发循环
if (!Object.is(v, cache[key])) void set(key, v) if (changed(v, cache[key])) void set(key, v)
}, },
{ deep: true }, { deep: true },
) )