React+TypeScript+Mobx Todo列表复选框快速切换闪烁问题求解
问题描述
我正在做一个Todo列表练习项目,客户端维护了一份Todo列表副本,每次网络请求后会通过fetchTodos函数更新本地数据。我在fetch请求完成后、本地存储Todo前用setTimeout模拟了网络延迟。
当快速切换多个不同Todo的完成状态时,每个Todo会出现闪烁,在已完成与未完成状态间反复切换。我清楚原因是第一个切换的更新操作在第二个切换触发后才完成,导致第二个切换更新前出现状态回滚的闪烁。
请问消除该闪烁的最佳方案是什么?
现有代码
fetchTodos 与 storeTodos 实现
async fetchTodos() { const response = await fetch('/todos'); if (!response.ok) { console.error('Failed to fetch tasks'); return; } const todos: Task[] = await response.json(); await new Promise(resolve => setTimeout(resolve, 1000)); //this.todos.splice(0, this.todos.length, ...todos); this.storeTodos(todos); } storeTodos(todos: Task[]) { for (let todo of todos) { this.todos[todo.id] = todo; } for (let id of Object.keys(this.todos)) { if (!todos.find(todo => todo.id === +id)) { delete this.todos[+id]; } } }
toggleTodo 实现
toggleTodo(id: number) { const task = this.findTask(id); if (task) { task.finished = !task.finished; fetch(`/todo/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task) }).then(response => { if (response.ok) { this.fetchTodos(); // 成功更新后重新获取所有Todo } else { console.error('Failed to update task'); } }).catch(error => { console.error('Failed to update task', error); }); } }
解决方案
1. 取消过时的请求
每次触发toggleTodo时,取消上一个未完成的fetchTodos请求,避免旧的延迟更新覆盖新的本地状态。使用AbortController实现:
// 在类内部添加AbortController实例变量 private fetchAbortController: AbortController | null = null; async fetchTodos() { // 取消之前未完成的请求 if (this.fetchAbortController) { this.fetchAbortController.abort(); } this.fetchAbortController = new AbortController(); try { const response = await fetch('/todos', { signal: this.fetchAbortController.signal // 绑定取消信号 }); if (!response.ok) { console.error('Failed to fetch tasks'); return; } const todos: Task[] = await response.json(); await new Promise(resolve => setTimeout(resolve, 1000)); this.storeTodos(todos); } catch (error) { // 忽略请求取消的错误 if (!(error instanceof DOMException && error.name === 'AbortError')) { console.error('Fetch error:', error); } } finally { this.fetchAbortController = null; } }
2. 优化更新逻辑,避免全量拉取
当前请求成功后全量拉取Todo列表的逻辑,会覆盖本地已经完成的新修改。改为请求成功后仅更新当前修改的Todo,同时增加失败回滚:
toggleTodo(id: number) { const task = this.findTask(id); if (!task) return; // 保存原始状态,用于请求失败时回滚 const originalState = task.finished; // 乐观更新:立即修改本地状态 task.finished = !task.finished; fetch(`/todo/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ finished: task.finished }) // 仅传递修改的字段 }).then(async response => { if (response.ok) { // 用后端返回的最新数据更新本地(如果后端返回完整Todo对象) const updatedTodo = await response.json(); this.todos[updatedTodo.id] = updatedTodo; } else { console.error('Failed to update task'); // 请求失败,回滚本地状态 task.finished = originalState; } }).catch(error => { console.error('Failed to update task', error); // 请求异常,回滚本地状态 task.finished = originalState; }); }
3. 防抖合并重复请求
短时间内多次触发fetchTodos时,用防抖函数合并为一次请求,减少不必要的更新:
// 防抖工具函数 function debounce<T extends (...args: any[]) => Promise<any>>(func: T, delay: number) { let timeoutId: NodeJS.Timeout | null = null; return (...args: Parameters<T>) => { if (timeoutId) clearTimeout(timeoutId); return new Promise<ReturnType<T>>((resolve, reject) => { timeoutId = setTimeout(async () => { try { const result = await func(...args); resolve(result); } catch (err) { reject(err); } }, delay); }); }; } // 在类内初始化防抖后的fetchTodos方法 private debouncedFetchTodos = debounce(() => this.fetchTodos(), 300); // 修改toggleTodo中的调用 toggleTodo(id: number) { // ... 原有逻辑 if (response.ok) { this.debouncedFetchTodos(); } // ... }
4. 移除开发阶段的模拟延迟
如果1秒的setTimeout仅用于开发时模拟网络延迟,上线前务必移除该代码,真实网络延迟不会如此固定且漫长,能大幅降低闪烁概率。
内容的提问来源于stack exchange,提问作者Fotis Kakogiannis
相关产品推荐
相关产品推荐

