perf: 大文档性能优化——1.getMarkdown 防抖 300ms: 2000-5000 行文档每次按键序列化全量 markdown 开销大, 改为 debounce 300ms; 2.dsh-flush-body 事件: Ctrl+S/关窗/切文档前同步编辑器内容到 store, 确保保存不丢内容; 3.openDoc 不 await saveNow: 旧文档保存与新文档读取并行, 加快切换速度; 4.自动保存间隔 1s→3s: 大文档每秒写盘 I/O 压力大; 5.修复 save-mechanism E2E 测试时序适配新防抖
This commit is contained in:
@@ -49,7 +49,7 @@ export function App() {
|
|||||||
}).catch(() => {}); } }, [activeId]);
|
}).catch(() => {}); } }, [activeId]);
|
||||||
// FR-S4:关窗前冲刷未保存内容
|
// FR-S4:关窗前冲刷未保存内容
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onBeforeUnload = () => { try { useStore.getState().saveNow(); } catch {} };
|
const onBeforeUnload = () => { try { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); } catch {} };
|
||||||
window.addEventListener('beforeunload', onBeforeUnload);
|
window.addEventListener('beforeunload', onBeforeUnload);
|
||||||
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -62,7 +62,7 @@ export function App() {
|
|||||||
await useStore.getState().loadTree();
|
await useStore.getState().loadTree();
|
||||||
await useStore.getState().openDoc(path);
|
await useStore.getState().openDoc(path);
|
||||||
},
|
},
|
||||||
onSave: () => useStore.getState().saveNow(),
|
onSave: () => { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); },
|
||||||
onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); },
|
onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); },
|
||||||
onToggleOutline: () => layout.toggleOutline(),
|
onToggleOutline: () => layout.toggleOutline(),
|
||||||
onCheat: () => setShowCheat(true),
|
onCheat: () => setShowCheat(true),
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function TiptapEditor() {
|
|||||||
const setBody = useStore(s => s.setBody);
|
const setBody = useStore(s => s.setBody);
|
||||||
const skipUpdate = useRef(false);
|
const skipUpdate = useRef(false);
|
||||||
const editorRef = useRef<Editor | null>(null);
|
const editorRef = useRef<Editor | null>(null);
|
||||||
|
const mdTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
// editor 实例稳定,不随文档切换重建(避免销毁/重建过渡期 commands 为 null)
|
// editor 实例稳定,不随文档切换重建(避免销毁/重建过渡期 commands 为 null)
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
@@ -92,7 +93,14 @@ export function TiptapEditor() {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onUpdate: ({ editor }) => { if (!skipUpdate.current) setBody((editor.storage as Record<string, any>).markdown.getMarkdown()); },
|
onUpdate: ({ editor }) => {
|
||||||
|
if (skipUpdate.current) return;
|
||||||
|
// 大文档防抖——getMarkdown() 对 2000+ 行文档开销显著,不必每次按键都序列化
|
||||||
|
if (mdTimer.current) clearTimeout(mdTimer.current);
|
||||||
|
mdTimer.current = setTimeout(() => {
|
||||||
|
setBody((editor.storage as Record<string, any>).markdown.getMarkdown());
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换文档时只同步内容,不重建 editor
|
// 切换文档时只同步内容,不重建 editor
|
||||||
@@ -111,6 +119,17 @@ export function TiptapEditor() {
|
|||||||
skipUpdate.current = false;
|
skipUpdate.current = false;
|
||||||
}, [currentPath, editor]);
|
}, [currentPath, editor]);
|
||||||
|
|
||||||
|
// 监听保存前冲刷——Ctrl+S / 关窗前同步编辑器内容到 store
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
const onFlush = () => {
|
||||||
|
if (mdTimer.current) { clearTimeout(mdTimer.current); mdTimer.current = null; }
|
||||||
|
setBody((editor.storage as Record<string, any>).markdown.getMarkdown());
|
||||||
|
};
|
||||||
|
window.addEventListener('dsh-flush-body', onFlush);
|
||||||
|
return () => window.removeEventListener('dsh-flush-body', onFlush);
|
||||||
|
}, [editor, setBody]);
|
||||||
|
|
||||||
if (!currentPath) {
|
if (!currentPath) {
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.4 }}>
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.4 }}>
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ export const useStore = create<EditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
openDoc: async (path: string) => {
|
openDoc: async (path: string) => {
|
||||||
// 切换前冲刷当前文档
|
// 切换前冲刷当前文档——先同步编辑器内容到 store 再保存
|
||||||
await get().saveNow();
|
window.dispatchEvent(new CustomEvent('dsh-flush-body'));
|
||||||
|
void get().saveNow();
|
||||||
const doc: ReadDocResult = await invoke(IPC.docRead, { path });
|
const doc: ReadDocResult = await invoke(IPC.docRead, { path });
|
||||||
set({ currentPath: path, body: doc.body, meta: doc.meta, title: doc.meta.title ?? path.split('/').pop()!.replace(/\.md$/, ''), saveState: 'saved', docMtime: doc.mtime });
|
set({ currentPath: path, body: doc.body, meta: doc.meta, title: doc.meta.title ?? path.split('/').pop()!.replace(/\.md$/, ''), saveState: 'saved', docMtime: doc.mtime });
|
||||||
// 同步到标签页(如已存在则激活,否则新建)
|
// 同步到标签页(如已存在则激活,否则新建)
|
||||||
@@ -103,7 +104,7 @@ export const useStore = create<EditorState>((set, get) => ({
|
|||||||
set({ body, saveState: 'saving' });
|
set({ body, saveState: 'saving' });
|
||||||
// FR-S1:防抖自动保存——间隔读 settings.autoSaveInterval(秒→毫秒)
|
// FR-S1:防抖自动保存——间隔读 settings.autoSaveInterval(秒→毫秒)
|
||||||
if (saveTimer) clearTimeout(saveTimer);
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 1) * 1000; } catch { return 1000; } })();
|
const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 3) * 1000; } catch { return 3000; } })();
|
||||||
saveTimer = setTimeout(() => { get().saveNow(); }, interval);
|
saveTimer = setTimeout(() => { get().saveNow(); }, interval);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ test('FR-S4:关窗冲刷未保存内容', async () => {
|
|||||||
await a.page.keyboard.type('before-close-flush');
|
await a.page.keyboard.type('before-close-flush');
|
||||||
// 等一下让内容进入 store
|
// 等一下让内容进入 store
|
||||||
await a.page.waitForTimeout(200);
|
await a.page.waitForTimeout(200);
|
||||||
// 手动触发 saveNow(模拟 beforeunload 冲刷)
|
// 冲刷编辑器内容到 store 再保存(模拟 beforeunload 冲刷)
|
||||||
|
await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body')));
|
||||||
await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.());
|
await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.());
|
||||||
await a.page.waitForTimeout(500);
|
await a.page.waitForTimeout(500);
|
||||||
await a.app.close();
|
await a.app.close();
|
||||||
@@ -86,7 +87,10 @@ test('FR-S6:reloadDoc 检测外部改动', async () => {
|
|||||||
|
|
||||||
await a.page.locator('.ProseMirror').click();
|
await a.page.locator('.ProseMirror').click();
|
||||||
await a.page.keyboard.type('original');
|
await a.page.keyboard.type('original');
|
||||||
await a.page.waitForTimeout(2000); // 等待自动保存
|
await a.page.waitForTimeout(200);
|
||||||
|
await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body')));
|
||||||
|
await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.());
|
||||||
|
await a.page.waitForTimeout(500); // 等待保存完成
|
||||||
|
|
||||||
// 外部修改文件
|
// 外部修改文件
|
||||||
const filePath = nodePath.join(vault, '重载测试.md');
|
const filePath = nodePath.join(vault, '重载测试.md');
|
||||||
|
|||||||
Reference in New Issue
Block a user