ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端通信与React核心:HTTP、WebSocket与状态更新全解

前端通信与React核心:HTTP、WebSocket与状态更新全解 如果你写过前端代码就一定躲不开“前端通信”这四个字。页面要拉数据表单要提交日志要上报服务端还要实时推消息甚至两个浏览器标签页之间也得互相递个话。而在 React 项目里这些问题往往还会跟上组件状态、渲染时机、生命周期函数这些概念纠缠在一起本来以为是网络问题查到最后发现是 setState 异步更新没搞明白。这篇东西打算把前端通信的主干方案梳理一遍再随手把 React 里那些面经高频基础题一起讲清楚适合刚接触 React 的开发者看也适合准备面试的人集中过一遍底子。1. 前端通信先从浏览器里的数据管道说起1.1 短连接时代的常规操作HTTP 请求前端通信里最老牌、使用率最高的就是 HTTP 请求。一个页面从服务端拿数据最常见的形态就是fetch或者 axios 发一个 GET/POST 请求服务端返回 JSON前端拿到之后把它喂给组件去渲染。这个模式非常简单直观前端发一次请求等一条响应整个过程结束。但越简单的模式在实际工程里越要抠细节。fetch是浏览器原生 API第一代用起来并不顺手比如它默认不会携带 cookiecredentials: include需要手动指定再比如fetch只有在网络层面报错时才会 reject服务端返回 500、404 它照样 resolve你得自己在response.ok里做判断。axios 之所以在社区里流行这么久一方面是因为它把 XHR 封装得舒服另一方面是拦截器、超时配置、取消请求这些能力开箱即用。项目里如果你们还在用原生 fetch建议自己封装一层统一处理错误码、token、超时这些逻辑。这里还牵出一个核心知识HTTP 协议本身的限制。HTTP/1.1 时代浏览器对同一域名并发连接数限制在 6 条左右请求多的时候只能排队队头阻塞问题也一直存在一个请求卡住会拖累后面排队的请求。HTTP/2 用多路复用解决了连接数和队头阻塞的一部分问题但 TCP 丢包恢复依然可能造成队头阻塞所以又过了几年QUIC HTTP/3 把传输层改成 UDP才彻底把这个老问题绕过去。这套演化逻辑可以解释很多前端问题比如为什么同一个页面里拆多个域名来放静态资源为什么现在没那么迫切了本质都是 HTTP 版本在背后发力。1.2 实时性要求上来之后短轮询、SSE 与 WebSocketHTTP 请求是“一锤子买卖”服务端想主动推数据给浏览器靠普通 HTTP 请求是做不到的。于是就有了几个经典方案。最早是短轮询前端用setInterval每隔几秒发一次请求问服务端有没有新数据。这个方案实现成本低但效率也低几十个客户端轮询一个接口绝大多数请求都是空转浪费带宽和服务器资源。长轮询稍微聪明一点客户端发请求之后服务端不立刻返回而是 Hold 住连接直到有数据才返回或者超时了再返回空。这样减少了大量无效请求但连接占用的时间更长服务器并发连接数压力不小。直到今天某些低实时性需求场景我仍然会选短轮询因为它简单、好调试、不容易出幺蛾子比如几秒钟才刷新一次的系统监控页面没必要为了“实时”两个字上重武器。SSEServer-Sent Events是一个经常被低估的方案。它基于 HTTP服务端返回Content-Type: text/event-stream客户端用EventSource接收。SSE 最核心的特点是单向服务端可以持续地向浏览器推送消息浏览器没法通过这条连接回传数据。实现难度很低断线了浏览器还会自动重连服务端只需要维护好连接列表就行。适合的场景是系统通知、股票行情、日志流这类“服务端单向下发”的业务。如果数据需要双向流动比如聊天消息、协同编辑、实时白板那就得上 WebSocket。WebSocket 在 TCP 上做了一次 HTTP 握手升级之后两端可以随时互相发数据是真正的全双工。客户端用法很简单new WebSocket(ws://...)然后在onopen、onmessage、onclose、onerror上挂处理函数。但实际工程里WebSocket 的坑远不止 API 那么简单。部署环境要确认网关和负载均衡是否支持 upgrade 协议如果用了 HTTPS 页面WebSocket 必须走wss://否则浏览器会直接拦截连接长时间空闲容易被中间设备回收所以通常需要心跳机制客户端隔一段时间发一个 ping服务端回一个 pong确保连接不会被掐断。这些都是上线以后最容易出问题的点。1.3 跨窗口与跨标签页通信postMessage、BroadcastChannel 与 storage 事件除了“浏览器与服务端通信”前端通信还有一个大类是“浏览器内不同窗口之间通信”。最常见的场景有两个一个是页面里嵌了 iframe父页面跟 iframe 内部应用要同步登录状态或操作指令另一个是用户开了多个标签页切换标签页时希望状态能同步。iframe 场景里最正统的方式是postMessage。发送方调用targetWindow.postMessage(data, targetOrigin)接收方在window.addEventListener(message, handler)中处理。注意这个targetOrigin参数非常重要它决定了消息哪个源可以收到。如果你传了一个固定 origin安全性会好很多如果你图省事传了*等于打开家门让任何页面都能把消息塞进你的监听器。接收消息的时候也要先判断event.origin是否来自可信来源不判断就处理数据跟拿着陌生人递过来的药就吃没什么区别。同源标签页之间通信BroadcastChannel是更轻量也更现代的选择。同一个浏览器中的多个标签页只要满足同源就可以注册同名 channel互相广播消息。它比postMessage的写法简单也比 localStorage 轮询方案实时性好。localStorage 本身也有storage事件但有个坑这个事件只会在“其他标签页”修改 localStorage 时触发当前页面自己改自己是不触发事件的。很多人第一次用它做跨页通信时明明改数据了却等不到回调就是因为这个机制没搞清。1.4 微前端架构里的通信设计如果你所在的项目用到微前端主应用和子应用之间的通信同样属于前端通信范畴。常见做法大体分三种第一种是主应用通过 props 把一些共享方法和数据传给子应用相当于“依赖注入”第二种是维护一个全局事件总线主应用和子应用都来订阅和发布事件第三种是干脆在内部封装一套基于window的自定义事件机制。这里要特别留意一件事不管是事件总线还是自定义事件通信协议最好统一收敛到一个独立的 SDK 或者工具包里不要让各个子应用各自定义一个事件名。子应用多了以后事件命名混乱会直接导致排查困难。你自己写还好团队十个人每人写一套命名规则线上出了 Bug 都不知道是谁在发消息。2. React 应用里的通信组织从 Props 到 WebSocket2.1 组件间通信的分层选择前端通信落到 React 应用里第一个绕不开的是组件之间的数据传递。最基础的是父子组件。父组件往子组件传 Props子组件通过回调函数通知父组件发生事件这个模式 React 官方也推荐好处是数据流方向清晰代码容易追踪。当层级加深中间层组件只是传递数据但其实根本用不到这些数据时逐层透传会变得很难看本地开发时还好组件一多就成了“props drilling”。遇到这种情况我优先考虑的是组件组合把需要共享数据的部分提取到共同父级或者干脆直接用 children 把结构颠倒一下让数据消费者尽量靠近数据源。跨层级组件共享数据React 原生方案是 Context。Context 的写法很简单一个 Provider 包起来多个消费组件就能直接取值。但 Context 有个必须注意的性能点Provider 的value如果每次渲染都生成新对象所有消费了该 Context 的组件都会跟着重新渲染即使其中有些组件根本只取了一部分数据。所以实际使用中要么用 useMemo 稳定 value要么把 value 拆成多个小 Context按需消费。再往上就是状态管理库了。我的选型判断标准是状态是否要被很多模块在组件树之外共享更新频率高不高。如果你的项目只是在一个页面内共享一些临时数据useStateuseReducer足够如果有跨页面、跨模块的共享数据比如用户信息和权限列表可以考虑 Zustand它非常轻store 可以在任何模块里访问也不依赖 Provider如果是大团队、多人维护、需要严格的状态流转规范Redux Toolkit 依然是靠谱选择。Jotai 这种方式也很灵活原子化设计把细粒度状态分散到各个模块适合画布类应用里每个节点都有自己的局域状态。2.2 接口请求到 React 状态的完整链路组件跟服务端通信最朴素的写法是在useEffect里发请求。但直接写很容易踩到竞态、内存泄漏、重复请求这几个坑。典型的第一版代码长这样useEffect(() { fetch(/api/user) .then(res res.json()) .then(data setUser(data)) .catch(err console.error(err)) }, [])这个写法在组件卸载后如果请求还没回来再执行setUser就会触发 React 的更新警告。现代 React 18 虽然不再像旧版本那样在控制台打红色警告但这依然意味着一次无效的 setState属于没有必要的浪费。更稳妥的做法是用 AbortController 取消请求useEffect(() { const controller new AbortController() fetch(/api/user, { signal: controller.signal }) .then(res res.json()) .then(data setUser(data)) .catch(err { if (err.name ! AbortError) { console.error(err) } }) return () controller.abort() }, [])这样组件卸载时请求被中断不会再做出无意义的 setState。但老实说现在大部分项目我不会手动去写这套逻辑直接上 React Query 或者 SWR 这种数据请求库更省心。它们把请求状态、缓存、重新验证、取消请求这些底层逻辑都收敛好了开发时只要声明 key 和 fetcher 函数就能拿到data、isLoading、error。因为它们内部有全局缓存机制多个组件请求同一个 key 的数据会自动去重天然避免重复请求。2.3 实时通信在 React 里的接入实践WebSocket 和 SSE 属于长连接接入 React 时要考虑跟组件生命的契合。一般做法是把连接实例放到一个 module 层级的单例或者 Store 中在某个顶层组件或者入口文件中建立然后通过 Context 或者状态管理库把消息推给需要的组件。需要注意 StrictMode 的坑。React 18 开发模式下组件挂载后 effect 会执行两次也就是说如果你在useEffect里直接new WebSocket()代码会建立两个连接。如果服务端没有做好去重开发环境就可能看到重复消息。解决办法是在 useEffect 里返回一个 cleanup 函数在卸载时关闭连接。这不仅是 StrictMode 的要求也是生产环境组件因为路由切换卸载后避免连接一直挂着的关键。SSE 的接入思路也类似。EventSource不需要手动管理重连但要记得在 cleanup 里调用close()。另外SSE 的消息事件名可以自定义默认是onmessage服务端也可以命名event: news客户端用addEventListener(news, handler)来监听。这个灵活性经常被忽略。实时消息推送到前端之后下一个问题是怎么让 UI 响应。我的建议是尽量走函数式更新比如setMessages(prev [...prev, newMessage])想拿最新消息做判断时最好在更新函数内部处理避免读到一个过期的 state。2.4 通信层错误处理与竞态问题实时通信的错误处理比普通请求复杂。WebSocket 连接不是一次性行为它可能会在运行中途断开断线之后客户端要决定是静默重连还是给用户提示还是做数据补偿。常见的做法是维护一个连接状态机connecting、open、reconnecting、closing。状态之间做迁移比如每次断线重连之前先更新 UI 为“连接中”让用户知道不是页面卡了。还有一类坑跟竞态有关。用户切换页面或者切换标签时WebSocket 消息到了但页面里对应的组件已经卸载这种消息要么丢弃要么存到一个全局缓冲里等组件重新挂载时再消费。数据一致性要求高的实时刷新场景比如在线表格编辑光靠本地状态管理还不行可能要引入版本号或者时间戳来做冲突检测。这部分说起来深入但真正做实时产品的人应该能感受到连接稳定性只是地基地基之上还得处理消息时序。3. 从生命周期到渲染通信数据如何在 React 中流转3.1 类组件生命周期函数三个阶段对应三类通信任务React 16.3 之前的生命周期函数用久了的人可能还记得那一长串名字componentWillMount、componentDidMount、componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、componentDidUpdate、componentWillUnmount。新版 React 里componentWillMount这类方法被标记了不安全不建议继续用。原因和 React 后来的并发渲染有关这些函数将来可能被调用多次期间组件可能又回到了旧的状态如果它里面包含副作用比如发起请求或订阅事件结果很容易错。最终的类组件生命周期主线其实很清晰挂载阶段constructor→getDerivedStateFromProps→render→componentDidMount更新阶段getDerivedStateFromProps→shouldComponentUpdate→render→getSnapshotBeforeUpdate→componentDidUpdate卸载阶段componentWillUnmount放到通信场景里核心时机就两个componentDidMount适合发起初次请求、建立 WebSocket、订阅事件componentWillUnmount负责断开连接、清除定时器、取消订阅。数据到达之后通过setState触发重新渲染。这个单向链路很直观理解了它很多面试问题就都有了抓手。3.2 Hooks 时代useEffect 如何接管生命周期函数组件没有生命周期函数Hooks 用useEffect把“副作用”收敛到一个统一 API 里。它可以等价替代很多类组件生命周期逻辑如果依赖数组传空数组effect 在挂载后执行一次cleanup 函数在卸载时执行类比componentDidMountcomponentWillUnmount如果依赖数组里有变量变量变化时 effect 重新执行类比组件更新后的通知。这里有几个高频误用点。第一依赖数组不能省略省略了 effect 每次渲染都执行如果里面发请求就变成每次渲染都请求一次后果大家都懂。第二不要在 effect 里读旧 state 来做条件判断依赖数组会把你绕进去。正确的姿势是让状态进入依赖数组或者使用useReducer把逻辑放在 reducer 里保证它是纯函数。第三useEffect在 DOM 绘制完成后异步执行所以如果你需要在浏览器绘制之前同步更新 DOM 布局比如测量节点尺寸并调整样式应该改用useLayoutEffect。这两个 Hook 执行时机的差异是面试官很喜欢埋伏笔的地方。3.3 渲染流程与通信数据的联动通信数据进入 React 之后会走一个标准化流程服务端返回数据 → 状态更新 → render 阶段生成新的元素树 → diff 对比 → commit 阶段更新真实 DOM。这套流程里通信数据只是触发源真正的渲染决策由 React 内部的调和算法完成。理解这个流程对实际调试非常有用。比如你发现 WebSocket 推送明明到了但页面没变化第一反应不应该是怀疑 React 有问题而是要检查 setState 有没有生效、更新的数据是不是和旧数据完全相同、组件有没有被 memo 包住。如果数据是对象的同一个引用React 会直接跳过渲染因为它的比较逻辑是浅比较prevState.next ! nextState.next判断为相等自然不会重渲染。所以做实时数据更新时尽量构造新的数组或对象不要直接 push 进旧数组然后 setState 同一个引用。4. React 基础问题解答面经里高频出现的那几道题4.1 setState 到底是同步还是异步这道题几乎每场 React 面试都会碰到。直接回答“是异步的”其实不够严谨严格来说 React 18 中是自动批处理语义更接近“离散更新”。在 React 18 之前事件处理函数里的 setState 会被批量合并表现为异步而在setTimeout、原生事件、Promise 回调里React 16 和 17 会同步处理setState 之后立刻能读到新值。这个不一致让很多人困惑但 React 18 改了规则几乎所有场景都会自动批量更新setTimeout 里也一样。这么做的好处是性能更优多个 setState 合并成一次渲染。但“自动批处理”带来的一个副作用是你调用 setState 之后立刻读取这个 state读到的还是旧值。如果你需要基于最新 state 做下一步操作正确做法是使用函数式更新setCount(prev prev 1)或者把依赖放在useEffect里等 state 更新后再执行。这个细节是高频考点也是写代码时天天遇到的坑。4.2 key 的作用到底是什么key 是 React 做列表 diff 的关键属性它让 React 可以识别同一次渲染中哪些元素是新增、哪些被删除、哪些保持原地复用。没有 key 或 key 使用不当React 只能按索引顺序对位比较很容易出现节点复用错误导致组件内部状态串位。最常见的反面教材是用数组 index 作为 key。列表头部插入一条数据时index 后面的所有元素 key 都变了React 会判定它们都不是同一个节点然后整个重建。如果每个列表项是简单文本重建问题也许不明显但如果列表项内部是一个带输入框的组件用户在这个输入框里输入了一半内容此时在列表头部插一条数据你会发现输入框里的内容错乱到别的行去了。原因就是节点被重建后React 把原始 DOM 移给了另一个 keystate 却没有跟着走。所以写列表时尽量用业务数据里稳定的 id 作为 key。如果是分页加载还不能只靠服务端给的 index页面翻回去列表重排index 毫无意义。这个问题的核心是key 的使命是给 React 提示“哪些节点在前后两次渲染中是同一个对象”优先级比“展示的内容是否相似”高得多。4.3 受控组件与非受控组件的选择受控组件的模式是valueonChange组件的值由 React state 全程管理用户改输入框会触发 onChange 事件然后你再决定要不要更新 state。非受控组件则不同它把值留在 DOM 自己手里React 只是渲染一个初始值之后通过ref去读 DOM 的真实值。受控组件的好处是单数据源校验、联动、格式化都方便是绝大多数表单场景的正确选择。非受控组件适合读取一次就能解决的场景比如文件上传的input你不需要实时监听选择文件这个动作只要在提交时读一下ref.current.files就行。另外有一点要注意不要在同一表单里混用受控和非受控否则会出现某个字段被 React 接管、另一个字段完全交给 DOM 管理的割裂状态排查起来非常费劲。4.4 合成事件机制与原生事件的区别React 自己封装了一套事件系统叫合成事件。React 17 之前所有事件都委托到 document 上React 17 之后改为委托到根容器上这样可以让多个 React 应用共存于同一页面而不互相干扰。合成事件的目的一个是统一浏览器差异避免频繁判断addEventListener的兼容性另一个是性能优化只需要在根容器上挂一次监听器不必为每个节点都绑定事件。合成事件一个容易踩的坑是它和原生事件的stopPropagation效果不一致。你在 React 的 onClick 里调用e.stopPropagation()只能阻止同层级的 React 合成事件继续冒泡但如果有人在原生 DOM 节点上绑了addEventListener(click, ...)这个原生监听器应用到 document 或者父节点上时React 合成事件的停止并不能阻止它。反过来原生事件里调e.stopPropagation()同样不能阻止 React 合成事件向父组件传递。所以如果在项目里混用原生事件和合成事件一定要清楚它们各自的事件流边界。5. React 周边热点的一次集中解读5.1 React 图表怎么选Canvas、SVG 还是 WebGLReact 生态里做图表方案多得让人眼花缭乱。核心要分清底层渲染技术SVG、Canvas、WebGL 三种它们的性能特征完全不同。SVG 用 DOM 节点描述图形优点是交互能力强可以直接用 CSS 样式、绑定 React 事件、实现无障碍阅读。缺点是节点数量超过一定层级之后性能急剧下降比如折线图几万个点全部渲染成path就很吃力。Canvas 用 JavaScript 绘制像素适合大数据量几万甚至几十万个点都能扛住但你要自己实现拾取、tooltip 这类交互逻辑。WebGL 则利用 GPU 渲染适合 3D 图表、实时大数据仪表盘开发复杂度明显更高对 WebGL 概念不熟的人不建议直接上手。业务上我的推荐顺序是交互复杂、数据量中等的后台管理系统直接上 ECharts它底层会根据数据量自动选择渲染方式配置项也非常全React 天然组件化程度高可以考虑 Recharts 和 visx它们都是基于 SVG 的。如果数据量非常大比如绘制高频实时曲线uPlot 这类轻量 Canvas 图表库更合适。另外一个画布场景也值得提如果做流程编排、节点编辑器react-flow 这个库对自定义节点支持得很好可以快速搭出类似工作流编排器那样的界面它内部也是用 React 渲染节点非常适合需要深度定制节点的产品。5.2 React Native 启动白屏怎么排查React Native 应用启动白屏是社区里吐槽很多的问题。最常见的原因是 JS Bundle 加载慢。应用启动后原生端要先加载 JS 代码这意味着要么从本地读取 bundle要么从远程服务器拉 bundle如果 bundle 很大或者设备性能弱白屏时间会非常明显。排查白屏我一般按顺序走先看原生端日志确认原生容器有没有启动成功、有没有 JS 加载错误如果原生没问题再看 JS 层入口组件有没有报错用 React Native Debugger 或者 Metro 的日志输出定位还有一种比较隐性的是 SplashScreen 没有在 JS 挂载完成后主动关闭导致原生启动画面盖在界面之上用户看到的就是一片空白。另外入口组件里如果有某些全局副作用在异步执行比如等待登录态检查也会出现首屏长时间空白。把这些地方挨个排查白屏的根因基本都能找到。5.3 React 与 AI Agent状态驱动模式的联想最近很多人聊“基于 React 模式构建能思考与行动的 AI 智能体”乍一听以为是 React 框架做 AI其实这里说的 React 是 AI Agent 领域里的 ReAct 范式指“推理 行动”循环跟 UI 库 React 只是同名。不过把一个 Ui 软件框架的思想用到 Agent 构建上确实有相通之处。Agent 需要感知输入、思考计划、调用工具、观察结果然后决定下一步行动这个循环本质上是一个状态机。我用 React 构建 Agent 前端时最容易落地的思路是把 Agent 的对话、工具调用结果、执行日志全部建模成不可变状态用状态变化来驱动 UI 更新。而 Agent 执行过程中的计划拆解和工具调用关系很适合用流程画布图来可视化这样一来前面的 react-flow 就派上用场了。整体思路还是 React 的那套规律搞清楚状态在哪里产生、在哪里消费用一个统一的 Store 管理起来视图自然就稳定。6. 通信落地过程中的常见问题与排查技巧实录6.1 高频问题速查日常开发里我积累了一张问题速查清单。下面这些场景几乎每周都能碰到按“现象、原因、处理方向”三列整理如下现象可能原因排查思路WebSocket 反复断开重连服务端空闲超时、中间代理回收连接抓包看 close code确认是否缺少心跳机制SSE 频繁自动重连服务端响应格式不对、网络不稳定检查 Content-Type 是否为 text/event-stream观察重连间隔postMessage 收不到消息targetOrigin 不匹配或监听时机过晚检查消息源先 addEventListener 再发送storage 事件一直不触发当前标签页修改 localStorage 本身不触发换不同标签页验证或改用 BroadcastChannel跨域请求被浏览器拦截CORS 头缺失或预检请求未通过观察 OPTIONS 请求响应确认允许的 Origin、Method、HeadersReact 中 setState 后立刻读不到新值自动批处理导致更新还未生效改用函数式更新或放到 effect 中读取组件卸载后仍收到消息长连接未在 cleanup 中关闭确保 useEffect 返回清理函数AbortController 取消请求开发环境请求出现两次React 18 StrictMode 下 effect 双执行关注 cleanup 函数连接销毁后重建6.2 几个我踩过坑之后的实操心得第一个心得是实时通信的方案尽量提前定下来不要在开发中切来切去。我见过一个项目先用了短轮询后来发现实时性不够改成 SSE后来又要求双向通信不得不把 SSE 换成 WebSocket。每换一次后端接口、前端状态管理、数据库策略都要跟着调整成本非常大。接手任何新项目第一件事永远是跟产品确认实时性要求再定方案而不是想当然地认为 WebSocket 一定比轮询好。第二个心得是WebSocket 重连不要无脑重试。服务端如果因为发布或者故障重启短时间内会有一大批客户端同时断线如果每个客户端都立刻重连会造成服务端压力瞬间飙升。稳妥一点的做法是带退避策略第一次断线等 1 秒第二次等 2 秒第三次等 4 秒最多等 30 秒。这样既不会让用户等太久又能把重连风暴对服务端的冲击降到最低。我自己的项目里还加了navigator.onLine检测如果浏览器本身离线连重连都不用触发。第三个想说的是React 应用里做通信不妨先“画数据流图”。通信本质上是数据在不同模块之间流动先把数据源、消费方、生命周期铺在一张图上写代码时就不会在 useEffect 里绕来绕去。我每次做实时协作类项目都会先把 WebSocket 的消息类型列成一张表标明每条消息由谁产生、谁消费、消费完是否要更新某个存储字段然后才动手写代码。这套习惯帮我避掉了很多“消息到了但页面不更新”的玄学问题也让我在看 React 面经题时不再觉得那是一堆零散知识点因为它们的底层逻辑就是代码里每天都在发生的那点事。
返回列表