You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 08:47:38