高并发下的列表乱序与文档同步

一、整体工作概述

今天主要完成了一点bug修复,高并发点击的时候列表会乱序,以及ai coding实现文档的CRUD,同时文档列表要同步渲染。 这一大块是ai写的,我只是看懂了代码,整体技术难度不高,而且一部分实现方式比较简单粗暴,后面还有优化空间。 简单记录一下开发过程,作为个人学习与复盘。

二、列表顺序随机跳变 bug 修复

  • 服务端非确定性行为与前端过度请求交织产生的 Bug 服务端存在非确定性行为,导致前端频繁点击时,骨架屏上的列表顺序会突然打乱,出现数据顺序随机跳变的 bug。

问题的本质是:

后端数据库查询时没有显式指定排序(例如按照什么sort),所以数据库返回数据的顺序没有强制保证,在高并发多次连接或缓存刷新时,顺序就会随机跳变。bug解决方案是是把下列useEffect删掉就好了。

ts 复制代码
  useEffect(() => {
    if (currentId) {
      fetchDocs(false);
    }
  }, [currentId, fetchDocs]);

为什么删除那个 useEffect 之后,选中文档列表的高亮还在,但乱序消失了?

高亮逻辑的真相: isActive = doc._id === currentId。

这里的 currentId 来自 URL(useParams)。当点击 Maps 时,URL 变了,React 会触发 WikiList 组件重新渲染。此时 docs 数组依然存在于内存中(State 未变),React 只需要重新计算每个 Item 的 isActive 布尔值并刷新 DOM 即可。根本不需要重新拉取数据。

乱序消失的真相: 既然不再触发 getAllDocuments,也就没有了后端随机排序的干扰,前端始终持有第一次加载时(第一个 useEffect)拿到的那份数据。

也就是说,前端连击触发 map,导致 current ID 改变,没法正常拿到后端数据,后端又重新返回一遍列表数据,把顺序打乱了。我每次都会触发全量数据拉取,后端又没有强制排序,数据顺序本身就不可靠;再加上 useEffect 频繁监听请求,拿到新顺序后重新渲染,最终导致列表顺序错乱。

这里我用 Network 面板验证,看了名为 documents 的请求响应,联机时能观察到同一个 ID 的元素位置在变化。 删掉这个 useEffect,不再触发 getAllDocuments 请求,不重新拉取数据,避开后端随机排序的影响,直接保留第一次加载的数据就行。

三、删除文档功能实现与疑问

在这之后,我做了第二个功能,这个功能是 AI 实现的,我只是把代码看了一遍,看懂后做了删除相关的操作。

我最初理解的全栈开发思路:首先在前端 API 服务层新增一个删除文档的接口供调用;前端 UI 事件触发时,在 onClick 里绑定这个 API 服务,向后端发请求。后端收到请求后,在 controller 层执行删除文件的指令,并新增对应的删除路由。

简化流程即:

前端 UI 绑定事件 → 事件触发请求 → 请求经前端 API 服务层发给后端 → 后端 controller 响应 → 路由分发 → 对数据库做 CRUD 操作。

这里我有个疑问:为什么后端新增路由就能直接操作数据库?是怎么直接连上数据库的?为什么现在后端的 POST、PUT、DELETE 这些路由都能直接操作数据库?

经过梳理,完整的全栈开发思路是:

前端 UI 绑定事件并触发 API 请求;请求经网络到达后端,由 "预设" 好的路由进行分发并指派给对应的控制器(Controller);控制器调用 "启动时已建立" 的数据库连接(借助.env 配置加载),最终完成对数据库的 CRUD 操作。

1. 前端层:定义与触发

API 服务层定义:先在前端 src/api 里写好"删除文档"的接口函数(比如用 axios.delete),明确告诉浏览器:我们要去哪个 URL、带哪个 id、用什么姿势(DELETE 方法)发请求。

UI 事件绑定:在 React 组件的按钮 onClick 里,直接绑定这个 API 函数。当用户一点,封装好的请求就带着数据"起飞",穿过网络直奔后端。

2. 后端层:接线与派发

路由分发 (Routing):这里要注意,后端是预先埋伏好路由的。请求一到,路由就像个"接线员",看一眼方法(DELETE)和路径,立刻把这个请求"踢"给对应的 Controller。

Controller 响应:Controller 才是真正的大脑。它负责把请求里的 id 摘出来,检查用户有没有权限删,然后下达最终的"处决指令"。

3. 持久层:指令执行

数据库 CRUD:Controller 调用数据库工具(比如 Mongoose 或 Prisma),利用早已建立好的连接管道,对数据库执行真正的删除动作。

Q1:为什么后端"配个路由"就能直接操作数据库?

路由只是"门牌号",不是"推土机"。 你之所以觉得"配了路由就能删",是因为你在写 router.delete() 的时候,紧跟着在回调函数里写了操作数据库的代码。 路由负责:确定"哪个接口对应哪个功能"。 Controller 负责:在这个功能里真正去改数据库。

Q2:后端是怎么"直接"连上数据库的?

连接发生在服务器启动的一瞬间,而不是请求来的时候。 当你运行后端项目(比如 npm run dev)时,程序会立刻读取 .env 里的数据库地址和密码,跟数据库建立一个持久的连接池。 平时:连接管道是通的,处于待命状态。 请求来时:Controller 只是顺着这个现成的"水管"发了一条指令,速度极快。

Q3:为什么现在的 POST、PUT、DELETE 路由都能操作数据库?

这是一种语义化约定 (RESTful),而不是物理限制。 从技术底层看,它们都是 TCP 数据包,本质没区别。但为了让代码好维护: DELETE:约定用来删,逻辑上直观。 POST/PUT:约定用来增和改,因为它们可以往请求体(Body)里塞进复杂的 JSON 结构,方便 Controller 拿到完整的数据去更新数据库。

四、细节优化与体验改进

还有一个细节问题:新增和删除文件时,左侧 wiki list 整个文档列表没有跟着数据动态更新。

当前处理方式是在 window 上挂载事件监听器,定义 DOCUMENTS_CHANGED_EVENT 处理文档变化。

ts 复制代码
export const DOCUMENTS_CHANGED_EVENT = "documents:changed";

export const notifyDocumentsChanged = () => {
  window.dispatchEvent(new Event(DOCUMENTS_CHANGED_EVENT));
};
ts 复制代码
  useEffect(() => {
    const handleDocumentsChanged = () => {
      fetchDocs();
    };

    window.addEventListener(DOCUMENTS_CHANGED_EVENT, handleDocumentsChanged);

    return () => {
      window.removeEventListener(
        DOCUMENTS_CHANGED_EVENT,
        handleDocumentsChanged,
      );
    };
  }, [fetchDocs]);

我觉得这个方式虽然清晰,但直接操作 DOM、挂载到 window 上,跳出了 React 框架,算不上优雅,实现简单但有点粗暴,后面可能会再改,想想怎么放到现有框架里,方便调试和维护。

另外还有 UX 细节比如:

  1. 删除当前文件后,不想显示空白占位 UI,希望自动打开列表里下一个文档。这里用了 if 判断,定义了 remainingDocs,逻辑大概是:如果还有数据,就从数组里依次读 ID 做路由跳转
ts 复制代码
			const remainingDocs = await getAllDocuments();
	          if (remainingDocs && remainingDocs.length > 0) {
            navigate(`/wiki/${remainingDocs[0]._id}`);
          } else {
            navigate("/wiki");
          }

只有数据全空时,才展示空白占位 UI。

  1. 还有一些警示弹窗,就是调用组件库组件Modal再配合国际化,没什么难度。

五、小结

今天做的工作难度不算大,解决问题的一些方案也比较粗暴,不够优雅。后面我会再想想更合适的实现方式,对架构的理解要更多一些,加油,加油!

参考文档

使用 Fetch - Web API | MDN

String.prototype.localeCompare() - JavaScript | MDN

Array.prototype.sort() - JavaScript | MDN

EventTarget.dispatchEvent() - Web API | MDN

相关推荐
LabVIEW开发4 小时前
LabVIEW QMH 队列消息处理架构
架构·labview·labview知识·labview功能·labview程序
代码搬运媛4 小时前
Jest 测试框架详解与实现指南
前端
counterxing5 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq5 小时前
windows下nginx的安装
linux·服务器·前端
rising start5 小时前
二、全面理解MySQL架构
mysql·架构
之歆6 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜6 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
麦客奥德彪6 小时前
Android Skills
架构·ai编程
Maimai108086 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
candyTong6 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构