Vue 里强制组件重新渲染:改 key 才是正解
Vue 里"刷新组件"是个模糊需求。要重新 render 一次?重新执行 setup 拉数据?还是彻底把内部 state 归零?不同目标要用不同手段。 四种方式的差别手段 会销毁组件 会重新执行 setup 会重置 state改 :key ✅ ✅ ✅$forceUpdate() ❌ ❌ ❌重新赋值 ref/reactive ❌ ❌ 只改被赋值的字段v-if 切换 ✅ ✅ ✅场景一:改 :key(推荐) 想"完全刷新"一个组件(比如切换 tab 后要重新拉数据、重置表单): <template> <UserForm :key="refreshKey" /> <button @click="refresh">重新加载</button> </template><script setup> import { ref } from "vue";const refreshKey = ref(0); const refresh = () => refreshKey.value++; </script>refreshKey 一变,Vue 视这是新组件——旧的销毁、新的挂载、setup() 重跑、生命周期钩子从 onMounted 全走一遍。 场景二:$forceUpdate(几乎用不到) 只想强制重新 render,不动 state: import { getCurrentInstance } from "vue"; const instance = getCurrentInstance(); instance.proxy.$forceUpdate();适用场景其实很窄——一般是把不响应的对象塞进模板,或者用了第三方非响应库。只要还能改成响应式,就别用 $forceUpdate。 场景三:v-if 切换 粗暴但清晰: <template> <UserForm v-if="show" /> </template><script setup> const show = ref(true); async function reset() { show.value = false; await nextTick(); show.value = true; } </script>等价于改 key,用于"父组件不方便传 key"的场景。 数据变了但界面不更新 Vue3 大多数场景响应式自动生效,出问题基本是下面几种: 1. 解构丢了响应式 const state = reactive({ count: 1 }); const { count } = state; // count 是快照,不再响应改成 toRefs: const { count } = toRefs(state); count.value++; // 会触发更新2. 用了 shallowRef 但改的是深层 const data = shallowRef({ a: 1 }); data.value.a = 2; // 不会更新手动触发: import { triggerRef } from "vue"; triggerRef(data);或者直接换整个引用: data.value = { ...data.value, a: 2 };3. 用了 markRaw / Object.freeze 这些对象上禁用响应式,任你怎么改视图都不动。 4. Vue 2 的经典坑(Vue 3 无此问题)this.arr[index] = value 不触发 → 用 this.$set 或 splice(index, 1, value) this.obj.newKey = 1 不触发 → 用 this.$set(this.obj, "newKey", 1)nextTick:拿最新 DOM 修改数据后立刻读 DOM 大小、滚动位置: import { nextTick } from "vue";count.value++; await nextTick(); console.log(el.value.scrollHeight); // 这时才是新高度一句话总结 "重置组件"改 :key、"只重画"用 $forceUpdate(且大概率你不该用它)、"数据不更新"先查是不是解构丢了响应式。
Vue 3 触发更新:key 强制重渲、triggerRef、nextTick 与解构丢失响应式
Vue 3 响应式自动更新 Vue 3 中 ref 和 reactive 的数据修改会自动触发组件重新渲染,无需手动操作: const count = ref(0); count.value++; // 自动触发更新const state = reactive({ name: 'hello' }); state.name = 'world'; // 自动触发更新强制重建组件:修改 :key 当需要完全销毁并重新创建一个组件(而不只是更新数据)时,修改 :key 是最推荐的方式: <template> <MyComponent :key="refreshKey" :data="data" /> <button @click="refreshKey++">强制刷新</button> </template><script setup> import { ref } from 'vue'; const refreshKey = ref(0); </script>:key 变化会让 Vue 销毁旧组件实例并创建新的,适合需要重置内部状态的场景。 shallowRef 深层修改:triggerRef shallowRef 只追踪引用本身的变化,不追踪对象内部属性: import { shallowRef, triggerRef } from 'vue';const data = shallowRef({ list: [] });data.value.list.push(1); // 不触发更新! triggerRef(data); // 手动通知 Vue 重新渲染非必要不用 shallowRef,直接用 ref 更简单。 nextTick:等待 DOM 更新完成 数据变更后,DOM 不会立即同步更新(Vue 批量异步更新),需要 nextTick 等待: import { ref, nextTick } from 'vue';const visible = ref(false); visible.value = true;await nextTick(); // 此时 DOM 已更新,可以安全操作 DOM 元素 const el = document.querySelector('.new-element');常见陷阱:解构 reactive 丢失响应式 const state = reactive({ count: 1, name: 'hello' });// 错误:解构后 count 是普通变量,不是响应式 const { count } = state; count; // 修改不会触发更新// 正确:用 toRefs 保留响应式 import { toRefs } from 'vue'; const { count, name } = toRefs(state); count.value++; // 正确触发更新ref 不存在这个问题,因为 ref 返回的是包装对象,count.value 始终访问同一个响应式引用。 Vue 2 迁移注意 Vue 2 中直接对数组索引赋值、添加新属性不响应,需要 $set: // Vue 2 this.$set(this.arr, 0, newValue); this.$set(this.obj, 'newKey', 1);Vue 3 用 Proxy 实现响应式,这两种写法都能正确追踪,不需要 $set。 $forceUpdate(不推荐) import { getCurrentInstance } from 'vue'; const instance = getCurrentInstance(); instance.proxy.$forceUpdate();$forceUpdate 只强制当前组件实例重渲染,不更新子组件,且不解决数据不响应的根本问题。遇到更新问题应优先排查响应式数据的使用方式,而不是调用 $forceUpdate。
页面快捷键监听不冲突:五条守则
给页面加自定义快捷键(Ctrl+K 打开搜索、Alt+1 切换 tab 之类),最容易踩的坑是和别的监听打架——把系统快捷键、编辑器热键、别的组件 hotkey 全干掉。列几条实用守则。 守则一:用 addEventListener,别赋值 错的写法: document.onkeydown = handler;这会覆盖之前的所有 keydown 监听——包括别的组件、别的库、浏览器扩展设置的。 正确写法: window.addEventListener("keydown", handler, { passive: false });// 组件卸载时移除 window.removeEventListener("keydown", handler);addEventListener 是往队列里追加,多个监听会一起执行。 守则二:判断 defaultPrevented Monaco Editor、CodeMirror、富文本组件、浏览器本身、系统快捷键——很多东西会调 e.preventDefault()。尊重它们: const handler = e => { if (e.defaultPrevented) return; // 别人已经处理了 // 你的逻辑 };不做这个判断,用户在 CodeMirror 里按 Ctrl+S 会既触发编辑器保存、又触发你的东西,行为不确定。 守则三:输入框里不触发 const t = e.target; const isEditable = t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t?.isContentEditable;if (isEditable) return;打字时的每个键都会走 keydown,你的 按 K 打开面板 会毁掉用户所有 K 字母的输入。 守则四:少用 stopPropagation 看到这种写法要警觉: e.stopPropagation();stopPropagation 阻止事件冒泡到父级,等于把父组件、全局快捷键全干死。 除非你明确需要(比如自己写一个 modal,希望里面的键盘操作不冒到外面),否则别用。 守则五:单字母触发要慎重 if (e.key === "s") { ... } // 危险s 太常见了——浏览器 Ctrl+S 保存、编辑器保存、任何输入框都会用到。要监听单字母,几乎必须搭配 modifier: if (e.ctrlKey && e.key.toLowerCase() === "k") { ... } if (e.altKey && e.key === "1") { ... } if (e.shiftKey && e.key === "?") { ... }或者监听几乎没人用的组合:F8、Ctrl+Shift+P、?(在没输入框时)。 Vue3 封装 import { onMounted, onUnmounted } from "vue";type KeyPredicate = (e: KeyboardEvent) => boolean;export function useHotkey(predicate: KeyPredicate, callback: (e: KeyboardEvent) => void) { const handler = (e: KeyboardEvent) => { if (e.defaultPrevented) return; const t = e.target as Element | null; if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || (t as HTMLElement)?.isContentEditable) return; if (predicate(e)) { e.preventDefault(); callback(e); } }; onMounted(() => window.addEventListener("keydown", handler, { passive: false })); onUnmounted(() => window.removeEventListener("keydown", handler)); }// 用法 useHotkey( e => e.ctrlKey && e.key.toLowerCase() === "k", () => openSearch(), );React 版 import { useEffect } from "react";export function useHotkey( predicate: (e: KeyboardEvent) => boolean, callback: (e: KeyboardEvent) => void, ) { useEffect(() => { const h = (e: KeyboardEvent) => { if (e.defaultPrevented) return; const t = e.target as HTMLElement; if (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable) return; if (predicate(e)) { e.preventDefault(); callback(e); } }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [predicate, callback]); }passive: false 是什么 现代浏览器为了滚动性能,某些事件默认变成 passive——你在里面调 e.preventDefault() 会报警告并被忽略。keydown 不受影响,但显式写 passive: false 是好习惯,声明"我可能会 preventDefault": window.addEventListener("keydown", h, { passive: false });对 keydown 来说是无用功但无害。对 touchstart / wheel 就必须写了。 特殊环境的坑Electron:主进程 globalShortcut 会拦截全局键,注意别覆盖用户系统的 iframe:跨域 iframe 里的 keydown 拿不到,需要 postMessage 转发 Monaco / CodeMirror:编辑器有自己的键映射系统,用它们提供的 API 注册,别在外面挂 keydown 浏览器扩展 content script:可能碰到页面已经拦截了某些键,要么 capture: true 抢先,要么改键位一句话总结 addEventListener 追加、检查 defaultPrevented、跳过输入框、避免 stopPropagation、单字母必须搭 modifier——五条守则遵守,你的快捷键就不会踩别人也不会被别人踩。
SQLite 大数据量优化:WAL 模式、批量事务、游标分页和索引策略
SQLite 单数据库文件理论上限约 281TB,实际几十 GB 均可正常运行,性能瓶颈通常来自配置和使用方式,而非 SQLite 本身。 必开:WAL 模式 PRAGMA journal_mode=WAL;默认的 DELETE journal 模式写入时会锁全库,WAL 模式允许读写并发,大幅提升写入性能。需要在每次连接时设置(或写入配置持久化)。 减少同步次数 -- 兼顾安全和性能(推荐) PRAGMA synchronous=NORMAL;-- 最高性能(断电可能丢最近几秒数据,非关键数据可用) PRAGMA synchronous=OFF;批量事务(最重要的优化) 单条 INSERT 性能极差,每次写入都会触发一次磁盘同步: // 错误写法:每条 INSERT 是独立事务 for (const item of items) { db.run("INSERT INTO logs VALUES (?)", item); }改为批量事务,性能提升 100-1000 倍: BEGIN; INSERT INTO logs VALUES (1, 'event_a', 1716800000); INSERT INTO logs VALUES (2, 'event_b', 1716800001); -- ... 批量写入 COMMIT;Node.js(better-sqlite3)示例: const insertMany = db.transaction((items) => { const stmt = db.prepare("INSERT INTO logs (id, type, ts) VALUES (?, ?, ?)"); for (const item of items) { stmt.run(item.id, item.type, item.ts); } });insertMany(items); // 一次事务写入全部Python(sqlite3)示例: conn.execute("BEGIN") conn.executemany("INSERT INTO logs VALUES (?, ?, ?)", items) conn.execute("COMMIT")建议每批 500-5000 条,批次过大会增加内存压力。 游标分页(替代 OFFSET) OFFSET 越大越慢,因为需要扫描并跳过前面所有行: -- 慢:OFFSET 100万,需要扫描 100万行 SELECT * FROM logs LIMIT 20 OFFSET 1000000;改用游标分页(Keyset Pagination): -- 快:直接从上次最后一条的 id 开始 SELECT * FROM logs WHERE id > ? LIMIT 20;记录上次最后一条的 id 作为下次查询的起始点,无论翻到多后面性能都一致。 索引策略 查看查询执行计划,确认是否走索引: EXPLAIN QUERY PLAN SELECT * FROM logs WHERE user_id = 123 AND created_at > 1716800000;原则:高频 WHERE 条件字段建索引 复合查询建复合索引(字段顺序与查询条件一致) 低选择度字段(如布尔值)单独建索引意义不大 写入密集型表控制索引数量,每个索引都会拖慢写入其他配置 -- 增大缓存(默认 -2000 即 2MB,建议改大) PRAGMA cache_size=-32000; -- 32MB-- 内存临时表(减少磁盘 I/O) PRAGMA temp_store=MEMORY;-- 预写 WAL 检查点 PRAGMA wal_autocheckpoint=1000;大字段(图片、视频、大 JSON、二进制 Blob)建议存文件系统,数据库只存文件路径,避免单数据库文件膨胀过快。
SQLite 存几十 GB 数据的六个优化点
有个错觉——SQLite 只适合小项目。其实很多人低估了它。理论上 SQLite 单库能到 281 TB,实际几十 GB、一亿行都能稳定跑。前提是这几个优化必须做。 先给个使用边界数据量 SQLite 适不适合< 10 GB 完全没问题,甚至比 Postgres 简单10 GB ~ 100 GB 可以,需要认真优化> 100 GB 谨慎,考虑分片 / 换数据库高并发写入 不推荐,SQLite 写是全库锁(WAL 缓解但仍单写)多机共享文件 别玩,NFS 上的 SQLite 特别容易坏适合 SQLite 的典型场景:本地缓存、日志、爬虫存档、IoT 边缘节点、桌面/Electron 应用、单机分析、中小型业务。 1. WAL 模式(必开) PRAGMA journal_mode = WAL;作用:读和写可以并发(默认 journal 会锁全库) 崩溃恢复快 大量写入性能大幅提升一次性开完之后写进库设置,不用每次连接都设。 2. 调 synchronous PRAGMA synchronous = NORMAL; -- 折中,推荐 -- PRAGMA synchronous = OFF; -- 极限性能,接受掉电丢数据FULL(默认)= 每次事务 fsync 磁盘、NORMAL = 检查点时 fsync、OFF = 完全信操作系统。日志/缓存场景 NORMAL 完全够用,性能差好几倍。 3. 批量事务(提升最猛的一条) 单条 insert: for (const row of rows) { db.prepare("INSERT INTO logs VALUES (?, ?)").run(row.a, row.b); }一亿行你等到明年。改成批量: const insert = db.prepare("INSERT INTO logs VALUES (?, ?)"); const many = db.transaction((rows) => { for (const row of rows) insert.run(row.a, row.b); }); many(rows);性能能差 100 ~ 1000 倍。SQLite 每个事务外面套一次 fsync,逐条 insert 就是每行都 fsync。 4. 索引不要滥建 EXPLAIN QUERY PLAN SELECT * FROM logs WHERE user_id = ? AND ts > ?;索引越多,写入越慢(每行 insert 都要维护索引) 只给高频过滤字段建索引 复合索引按 WHERE 里最常见的过滤顺序建:(user_id, ts) 大文本 / JSON 字段别建普通索引,需要就用 FTS55. 分页别用 OFFSET -- 慢,越翻越慢:OFFSET 需要跳过前 N 行 SELECT * FROM logs ORDER BY id LIMIT 20 OFFSET 1000000;-- 快,游标分页:直接从上一页最后一个 id 之后取 SELECT * FROM logs WHERE id > ? ORDER BY id LIMIT 20;学名叫 Keyset Pagination,任何大表分页都该用。 6. 大字段不要塞 blob 图片、视频、大 JSON 直接扔 SQLite 会拖慢一切——包括那些跟大字段无关的查询。 正确做法:文件系统 / 对象存储放实体 SQLite 只存路径 / 元数据或者用 SQLite 官方推荐 的经验值:< 100 KB 存库里更快,> 100 KB 存文件系统更快。 7. 加上几个 PRAGMA PRAGMA cache_size = -100000; -- 100 MB 缓存(负数表示 KB) PRAGMA temp_store = MEMORY; -- 临时表放内存 PRAGMA mmap_size = 268435456; -- 256 MB mmap,减少 syscall PRAGMA busy_timeout = 5000; -- 遇到锁等 5 秒再报错一句话总结 WAL + 批量事务 + Keyset 分页——这三条打下来,几十 GB 的 SQLite 一点都不难。别忘了大字段挪出去、别乱建索引。
