阅读视图

发现新文章,点击刷新页面。

React动画方案对比:CSS动画和Framer Motion和React Spring

1. 前言

在现代 Web 应用中,动画是提升用户体验的重要手段。React 生态中提供了多种动画实现方案,每种方案都有其适用场景和技术特点。本文将深入对比三种主流方案:原生 CSS 动画、Framer Motion 和 React Spring,通过实现同一动画效果展示各方案的优缺点,帮助你在项目中做出最佳选择。

2. CSS 动画

CSS 动画是实现简单过渡效果的最直接方式,无需引入额外依赖,性能表现优秀。

2.1. 基本实现方式

通过 CSS 类切换或 @keyframes 实现动画:

import React, { useState } from 'react';

function FadeInComponent() {
  const [show, setShow] = useState(false);
  
  const toggle = () => {
    setShow(!show);
  };
  
  return (
    <div>
      <button onClick={toggle}>显示/隐藏</button>
      <div 
        className={`fade-element ${show ? 'visible' : 'hidden'}`}
      >
        渐显渐隐元素
      </div>
    </div>
  );
}

// CSS 样式
.fade-element {
  opacity: 0;
  transition: opacity 0.5s ease;
}

.fade-element.visible {
  opacity: 1;
}

2.2. 复杂动画实现

使用 @keyframes 实现更复杂的动画效果:

@keyframes slideIn {
  0% { transform: translateX(-100%); opacity: 0; }
  100% { transform: translateX(0); opacity: 1; }
}

.slide-in {
  animation: slideIn 0.5s forwards;
}

2.3. 优缺点分析

  • 优点

    • 实现简单,无需额外学习成本
    • 性能最优(由浏览器直接优化)
    • 适合简单的过渡效果(如淡入淡出、缩放)
  • 缺点

    • 缺乏 JavaScript 控制能力(如暂停、反向播放)
    • 复杂动画(如物理动效)实现困难
    • 状态管理复杂(需维护多个 CSS 类)

3. Framer Motion

Framer Motion 是专为 React 设计的动画库,提供了强大的 API 和直观的组件化接口,适合复杂交互场景。

3.1. 基础使用

下面是一个简单过渡动画:

import { motion } from 'framer-motion';
import React, { useState } from 'react';

function FadeInComponent() {
  const [show, setShow] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShow(!show)}>显示/隐藏</button>
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: show ? 1 : 0 }}
        transition={{ duration: 0.5 }}
      >
        渐显渐隐元素
      </motion.div>
    </div>
  );
}

3.2. 复杂动画

下面是一个拖拽与弹簧效果:

import { motion, useDragControls, useSpring } from 'framer-motion';

function DraggableBox() {
  const dragControls = useDragControls();
  const { x, y } = useSpring({
    x: 0,
    y: 0,
    config: { tension: 200, damping: 20 }
  });
  
  return (
    <motion.div
      drag
      dragControls={dragControls}
      dragConstraints={{ left: 0, right: 300, top: 0, bottom: 200 }}
      style={{ x, y }}
    >
      可拖拽元素
    </motion.div>
  );
}

3.3. 路由过渡动画

import { motion, AnimatePresence } from 'framer-motion';
import { Routes, Route, useLocation } from 'react-router-dom';

function AnimatedRoutes() {
  const location = useLocation();
  
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={location.pathname}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3 }}
      >
        <Routes location={location}>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </motion.div>
    </AnimatePresence>
  );
}

3.4. 优缺点分析

  • 优点

    • 功能全面,支持复杂动画(如拖拽、滚动触发、3D变换)
    • 声明式 API,代码简洁易维护
    • 良好的类型支持和文档
    • 支持与 React 生命周期深度集成
  • 缺点

    • 包体积较大(约 14KB gzipped)
    • 学习曲线较陡(需理解各种动画概念)
    • 性能略低于原生 CSS 动画

4. React Spring

React Spring 专注于实现自然流畅的物理动效,适合需要精细控制动画物理学特性的场景。

4.1. 基础使用

下面是一个弹簧动画:

import { useSpring, animated } from 'react-spring';

function SpringButton() {
  const props = useSpring({
    from: { opacity: 0, transform: 'scale(0.8)' },
    to: { opacity: 1, transform: 'scale(1)' },
    config: { tension: 170, friction: 26 }
  });
  
  return (
    <animated.button style={props}>
      弹簧按钮
    </animated.button>
  );
}

4.2. 交互触发动画

import { useSpring, useTrail, animated } from 'react-spring';

function TrailAnimation() {
  const trail = useTrail(5, {
    from: { opacity: 0, transform: 'translate3d(0,-40px,0)' },
    to: { opacity: 1, transform: 'translate3d(0,0,0)' },
  });
  
  return (
    <div>
      {trail.map((style, index) => (
        <animated.div key={index} style={style}>
          Item {index + 1}
        </animated.div>
      ))}
    </div>
  );
}

4.3. 滚动触发动画

import { useScroll, animated } from 'react-spring';

function ScrollAnimation() {
  const { scrollYProgress } = useScroll();
  
  return (
    <animated.div 
      style={{
        opacity: scrollYProgress,
        transform: scrollYProgress.interpolate(
          (y) => `translate3d(0, ${y * 50}px, 0)`
        )
      }}
    >
      滚动触发动画
    </animated.div>
  );
}

4.4. 优缺点分析

  • 优点

    • 专注于物理动效,提供丰富的物理学参数配置
    • 性能优秀,适合高频动画(如滚动、拖拽)
    • 轻量级(约 8KB gzipped)
    • 支持与其他库(如 Three.js)集成
  • 缺点

    • API 相对底层,学习成本较高
    • 缺乏内置组件(如 Framer Motion 的 AnimatePresence
    • 文档和社区资源不如 Framer Motion 完善

5. 性能对比与场景选择

性能对比如下:

方案 体积(gzipped) 简单动画性能 复杂动画性能
CSS 动画 0KB ✅✅✅
Framer Motion ~14KB ✅✅ ✅✅✅
React Spring ~8KB ✅✅ ✅✅✅

场景选择如下:

  • 推荐使用 CSS 动画的场景

    • 简单的过渡效果(如淡入淡出、悬停效果)
    • 无需 JavaScript 控制的纯视觉动画
    • 性能敏感的高频动画(如滚动指示器)
  • 推荐使用 Framer Motion 的场景

    • 复杂交互驱动的动画(如拖拽、缩放、路由过渡)
    • 需要丰富的布局动画(如列表项进入/退出动画)
    • 与 React 组件深度集成的动画
  • 推荐使用 React Spring 的场景

    • 需要精细控制物理参数的动画(如弹簧、阻尼效果)
    • 轻量级应用,对包体积敏感
    • 与其他动画库或 3D 库结合使用

6. 实战案例

下面通过实现一个模态框动画,对比三种方案的实现差异:

6.1. CSS 动画实现

// 组件代码
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
        <button onClick={onClose}>关闭</button>
      </div>
    </div>
  );
}

// CSS 样式
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  animation: fadeIn 0.3s forwards;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
  transform: scale(0.8);
  animation: scaleIn 0.3s forwards;
}

@keyframes fadeIn {
  to { opacity: 1; }
}

@keyframes scaleIn {
  to { transform: scale(1); }
}

6.2. Framer Motion 实现

import { motion } from 'framer-motion';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  return (
    <motion.div
      className="modal-overlay"
      onClick={onClose}
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      <motion.div
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
        initial={{ scale: 0.8 }}
        animate={{ scale: 1 }}
        exit={{ scale: 0.8 }}
        transition={{ duration: 0.3 }}
      >
        {children}
        <button onClick={onClose}>关闭</button>
      </motion.div>
    </motion.div>
  );
}

6.3. React Spring 实现

import { useSpring, animated } from 'react-spring';

function Modal({ isOpen, onClose, children }) {
  const { opacity, scale } = useSpring({
    opacity: isOpen ? 1 : 0,
    scale: isOpen ? 1 : 0.8,
    config: { duration: 300 },
  });
  
  if (!isOpen) return null;
  
  return (
    <animated.div
      className="modal-overlay"
      onClick={onClose}
      style={{ opacity }}
    >
      <animated.div
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
        style={{ transform: scale.interpolate(s => `scale(${s})`) }}
      >
        {children}
        <button onClick={onClose}>关闭</button>
      </animated.div>
    </animated.div>
  );
}

7. 总结

选择合适的动画方案对 React 应用的性能和用户体验至关重要。本文通过对比分析得出以下结论:

  1. CSS 动画:简单、高效,适合无交互的基础动画,是轻量级应用的首选。
  2. Framer Motion:功能全面、API 友好,适合复杂交互场景和大型应用。
  3. React Spring:专注物理动效,适合需要精细控制动画物理学特性的场景。

在实际项目中,建议根据动画复杂度、性能需求和团队技术栈综合选择。对于大多数场景,Framer Motion 提供了最佳的平衡;而对于追求极致性能的简单动画,CSS 动画仍是最优解。


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

Next.js的水合:静默的页面“唤醒”术

大家好,我是大鱼。今天我们要聊一个Next.js中极为重要却又容易被忽视的概念——水合。这不是化学课,但理解它能让你的Next.js应用“活”起来!

什么是水合?

想象一下,你在网上买了一个宜家书架。快递送到时,所有木板都已经切割好、打好孔,甚至部分组装好了——这就是服务端渲染

但此时的书架还不能用:隔板无法调节,抽屉不能滑动。直到你按照说明书,拧上最后的螺丝,安装好滑轨——这个过程就是水合

在Next.js中,水合指的是:在浏览器端,将React组件的交互逻辑“附加”到服务端预渲染的静态HTML上的过程

深入水合的全过程

第一阶段:服务端渲染

当用户请求页面时,Next.js服务器会:

  • 执行React组件代码
  • 生成完整的HTML字符串
  • 直接返回给浏览器

此时用户看到的是完整的页面,但无法交互——就像看到了组装一半的书架。

第二阶段:资源加载

浏览器在展示静态HTML的同时,在后台默默加载页面所需的JavaScript包。

第三阶段:水合激活

这是最关键的一步:

// React在背后做的事情大致如下
function hydrate(serverHTML, clientComponents) {
    // 1. 对比服务端HTML与客户端组件
    // 2. 复用现有的DOM节点
    // 3. 附加事件处理器和状态管理
    // 4. 让页面变得可交互
}

水合完成后,你的页面就从一个静态文档变成了功能完整的React应用。

为什么水合如此重要?

1. 极致的首屏性能 用户不需要等待所有JS加载完成就能看到内容,大幅提升首次渲染速度。

2. 无缝的体验过渡 从静态内容到交互应用的转换是平滑的,用户几乎感知不到。

3. SEO优化 搜索引擎可以直接抓取到完整内容,而不是一个空壳。

水合的“坑”与解决之道

在实际开发中,我们最常遇到的就是“水合不匹配”错误。

常见问题场景

场景一:使用了浏览器特有API

// 错误示例
function UserProfile() {
    // 服务端没有localStorage,会导致水合失败
    const user = localStorage.getItem('user');
    return <div>Hello, {user.name}</div>;
}

// 正确做法
function UserProfile() {
    const [user, setUser] = useState(null);
    
    useEffect(() => {
        // 只在客户端执行
        const userData = localStorage.getItem('user');
        setUser(userData);
    }, []);
    
    return <div>Hello, {user?.name || 'Guest'}</div>;
}

场景二:时间或随机数差异

// 问题代码
function UniqueComponent() {
    // 服务端和客户端生成的ID不同
    const id = Math.random().toString(36);
    return <div id={id}>内容</div>;
}

// 解决方案
function UniqueComponent() {
    const [id, setId] = useState(null);
    
    useEffect(() => {
        setId(Math.random().toString(36));
    }, []);
    
    return <div id={id || 'server-id'}>内容</div>;
}

实用调试技巧

当遇到水合错误时,可以:

  1. 检查控制台警告:React会详细指出不匹配的位置
  2. 使用React DevTools:查看组件树和状态差异
  3. 对比HTML源码:分别查看服务端返回的HTML和水合后的DOM

最佳实践指南

经过多个项目的实践,我总结出以下经验:

1. 组件设计原则

  • 保持服务端和客户端渲染的一致性
  • 避免在渲染逻辑中使用浏览器特有API
  • 对动态内容使用条件渲染

2. 性能优化

// 使用动态导入延迟加载非关键组件
const HeavyComponent = dynamic(
    () => import('./HeavyComponent'),
    { 
        ssr: false, // 不需要服务端渲染
        loading: () => <div>加载中...</div>
    }
);

3. 状态管理

  • 使用Next.js的getServerSideProps进行服务端状态初始化
  • 客户端状态在useEffect中处理

总结

水合是Next.js架构中的精髓所在。它巧妙地将服务端渲染的性能优势与客户端渲染的交互体验结合在一起。

互动时间

你在项目中遇到过水合相关的问题吗?或者有什么独特的优化经验?欢迎在评论区分享交流!


PS: 如果你觉得这篇文章有帮助,欢迎点赞、转发。

时间切片 + 双工作循环 + 优先级模型:React 的并发任务管理策略

前言

React 作为著名的 UI 构建库,快速响应是其特点之一。然而 JS 作为单线程语言,在运行某个任务时,会阻塞主线程对于其他事件的响应,针对此特点,React 制定了特定的任务管理策略,以支持并发的任务管理策略。

本文将梳理 React 如何实现并发特性,及如何调度不同任务的执行顺序。

并发 & 并行

在文章正式开始前,先简单梳理一下计算机科学中的并发和并行。

它们分别描述了两种对任务的不同处理方案:

  • 并发:单个处理器通过时间片轮转的方式,实现多个任务交替执行,由于每个时间片很短,看起来像多个任务同时执行。
  • 并行:多个处理器同时执行不同任务。

以生活中的例子举例,假设有一家服装店:

  • 并发就像店里只有一名工作人员,他轮转地为顾客介绍商品、收款、打包货物。
  • 并行则像店里有多名工作人员,分别负责介绍商品、收款、打包货物。

并发的特点在于及时响应,并行则在于同时处理。

并发特性概述

在 React 中,由于状态的变更(如 setState 的调用)所导致页面的重新渲染可以看作是一个任务(渲染任务)。
在 React16 之前,有两个核心问题:

  1. 渲染任务不可中断,无法及时响应用户的操作,造成应用卡顿的风险。
  2. 渲染任务无法根据优先级排序,后面触发的高优先级任务需要等待之前的低优先级任务执行完毕之后才能执行,造成用户体验不佳。

为了解决以上问题,React16 推出了 Fiber 架构,并且基于 Fiber 架构将不可中断的 stack reconciler 重构为可中断的 fiber reconciler,并且辅以优先级系统,优先响应高优先级任务,优化用户体验。

以上的解决方案,正是 React 并发特性(Concurrent Features)的基础。

React 的并发特性是一种渲染策略,旨在提升应用的响应能力。它让 React 在执行渲染任务的时候仍保持对其事件的响应能力,且可以根据任务优先级中断当前工作,优先处理高优先级的任务(如点击、输入),之后再恢复低优先级的渲染任务。

React 之所以能拥有并发能力,底层依靠以下三个概念:

  • Fiber 架构 —— 底层架构
    • 需要实现并发能力,重点是可中断/恢复的渲染。Fiber 架构为可中断渲染提供了底层数据结构的支持。将整个更新任务拆分为多个工作单元,每个 Fiber 节点代表一个工作单元。从数据结构上让可中断渲染成为可能。
  • Scheduler —— 架构驱动
    • Fiber 架构为可中断渲染提供了数据结构上的支持,同时也需要一个新的调度方式与之匹配,去控制渲染过程(否则还是使用旧的同步运行方式,Fiber 架构将无法发挥能力)
    • 借助时间切片,控制任务的执行时间,防止长期占用主线程,Scheduler 则提供了此能力。
  • Lanes 模型 —— 任务优先级策略
    • 不同的任务分为不同的优先级。高优先级任务可以打断低优先级任务,以实现重要任务的及时响应。Lanes 模型为不同任务赋予不同优先级,配合时间切片实现高优先级任务打断低优先级任务的能力。

接下来将介绍上述三者是如何配合实现渲染过程不阻塞主线程,及高优先级任务打断低优先级任务这两个并发特性需要具备的底层能力。

优先级系统

优先级系统用于区分任务的紧急程度,React 根据任务不同的优先级安排不同的执行执行时机,也是实现高优先级任务打断低优先级任务的依据。

Lanes 优先级系统

React 自身拥有 Lanes 优先级,在 Fiber 节点中以 lanes 属性记录。

Lanes 优先级系统使用二进制数字代表优先级,数字越小优先级越高。

如下节选部分优先级,完整优先级见这里

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;

export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;

事件优先级

在 React 中,还有另外一套优先级系统——事件优先级。

React 中的事件都是包装过的合成事件,每种事件都会带有不同的优先级。我们通过点击或者其它事件出发绑定的监听事件的时候就会带上对应的优先级。

使用 getEventPriority 函数,通过事件名称获取对应的优先级。

export function getEventPriority(domEventName: DOMEventName) {
  switch (domEventName) {
    // ...some code...
    case "cancel":
    case "click":
      return DiscreteEventPriority;
    default:
      return DefaultEventPriority;
  }
}

Scheduler 优先级系统

Scheduler 是一个独立的任务调度系统,所以它拥有自己的优先级系统。

Scheduler 的优先级系统将任务的优先级分为了:

const ImmediatePriority = 1;
const UserBlockingPriority = 2;
const NormalPriority = 3;
const LowPriority = 4;
const IdlePriority = 5;

Scheduler 会根据任务不同的优先级,分配不同的过期时间,如下节选部分 unstable_scheduleCallback 函数代码:

var maxSigned31BitInt = 1073741823;

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

function unstable_scheduleCallback() {
  // ...some code...

  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  var expirationTime = startTime + timeout;

  // ...some code...
}

优先级之间的互相转换

  • Lanes 优先级转事件优先级:通过函数lanesToEventPriority实现,传入 lanes 优先级即可得到对应的事件优先级

  • 事件优先级转 scheduler 优先级:通过上面的 unstable_scheduleCallback 函数中的 switch 语句转换

一般而言,我们只需要了解 Lanes 优先级和事件优先级是 React 中的优先级,而 Scheduler 优先级是 Scheduler 库自身的优先级。

React 中的任务最终需要通过 Scheduler 进行调度,所以当 React 中触发了某个事件,赋予了优先级之后,需要进行Lanes优先级 -> 事件优先级 -> Scheduler优先级的转换。

Fiber 架构

Fiber 架构在之前的文章中有聊到,本文只简单提及其核心作用。

在 Fiber 架构被创造出来之前,React 的渲染任务是一整个任务,即一旦开始执行便不可暂停与恢复。

为了防止渲染过程阻塞主线程,需要设计渲染任务可暂停与恢复的渲染架构。于是 Fiber 架构就被创造出来了,为 React 的可中断渲染提供数据结构上的支持。

每个 Fiber 节点代表一个组件节点或原生元素节点,同时也代表渲染任务中的一个工作单元。依靠 child、sibling、return 节点形成链式树状结构,从结构上支持遍历的中断与恢复(知道自己从哪里来,该往哪里去)。

在执行渲染任务的过程中,可以以工作单元(Fiber 节点)为颗粒度暂停渲染,让出主线程转而去响应其他事件,处理完毕后再回来恢复渲染任务。(此过程的工作单元的执行与暂停需要 Scheduler 进行调度,Fiber 架构只提供数据结构上的支持)。

image.png

Scheduler

Scheduler是一个功能上独立于 React 的依赖包,主要实现了时间切片优先级系统,用于控制任务的执行过程,其官方描述为:

This is a package for cooperative scheduling in a browser environment. It is currently used internally by React, but we plan to make it more generic.
可译为: 这是一个用于在浏览器环境中进行协作式调度的包。目前它被 React 内部使用,但我们计划使其更加通用。

React 使用此依赖包进行任务的调度,使任务的执行不会长期阻塞主线程,提供并发特性的底层支持。

任务创建与调度

Scheduler 通过暴露 unstable_scheduleCallback 函数,给使用者创建任务,并自动进行调度。

function unstable_scheduleCallback(priorityLevel, callback, options) {}

unstable_scheduleCallback 会创建任务并加入到任务队列中,然后调用 schedulePerformWorkUntilDeadline 函数进行调度。

schedulePerformWorkUntilDeadline 函数如下所示,会根据不同的环境选择不同的调度方案,在正常浏览器中,会使用 MessageChannel 发布任务调度的消息。

MessageChannel 实例拥有两个端口 port,可以分别监听和发送消息。
当监听函数被触发时,会作为宏任务加入宏任务队列,需浏览器的事件循环机制进行调度执行。

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === "function") {
  // Node.js and 旧版本IE.
  // ...some code...
} else if (typeof MessageChannel !== "undefined") {
  // 浏览器环境,使用MessageChannel

  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  const channel = new MessageChannel();
  const port = channel.port2;
  // 监听任务调度的信息,并执行performWorkUntilDeadline
  channel.port1.onmessage = performWorkUntilDeadline;
  // 发布任务调度消息
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // 低版本浏览器
  // ...some code...
}

MessageChannel 将任务调度加入到宏任务队列中,浏览器将通过事件循环机制,在合适的事件调用此宏任务,即执行上面代码中的 performWorkUntilDeadline 函数。

performWorkUntilDeadline 中将会正式调用 scheduledHostCallback 执行渲染任务(具体执行方式见文章后续的 workLoop),并且通过其返回值判断是否有剩余任务,如果有的话,则通过 MessageChannel 重新发起调度,等待浏览器事件循环机制执行,确保不会阻塞主线程。

const performWorkUntilDeadline = () => {
  //
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
      // scheduledHostCallback的核心就是执行下面将会提到的workLoop,它将会返回workLoop的返回值。
      // 如果返回值 hasMoreWork 为true就说明任务没执行完还要发起下一次调度
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // 如还有剩余任务,则重新请求调度(即上面提到了MessageChannel)
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};

时间切片

时间切片的含义在于,规定时间片的长度,每执行完一个任务后,检查本轮耗时是否超过时间片范围,如超过则让出主线程,并在下一轮事件循环中继续执行任务。

实现时间分片的主要函数之一为 shouldYieldToHost,它的作用在于检测当前时间切片的时间是否耗尽,是否需要让出主线程。

function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  // frameInterval为时间片,React中默认为5ms
  if (timeElapsed < frameInterval) {
    // The main thread has only been blocked for a really short amount of time;
    // smaller than a single frame. Don't yield yet.
    return false;
  }

  // ...some code...
}

工作循环

实现时间分片功能,除了上述判断当前时间片是否耗尽的函数以外,还需要使用循环来控制任务的执行及中断。如下图所示:

image.png

即任务列表中的任务并非以同步的方式一次性执行,而是每执行完一个任务后,判断时间片是否耗尽,再决定继续执行任务还是让出主线程,等待下一次任务调度。

Scheduler 中的工作循环

在 React 中,每当状态改变而触发的渲染任务会存放在任务队列 taskQueue 中,我们不能一次性地清空任务队列(可能会阻塞主线程,引起应用卡顿),而应该使用循环配合时间片的方式去调度任务的执行。

而负责调度 taskQueue 执行的调度器则是 Scheduler,它控制的循环可称为任务调度循环

具体体现为 workLoop 函数,此循环会不断从任务队列中取出任务执行,并且调用 shouldYieldToHost 函数进行判断,在适当时机让出主线程。 以下为 workLoop 函数节选,完整代码在这里阅读

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime); // 通过小顶堆获取优先级最高的任务
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 判断是不是过期
      // 任务没有超时并且时间片时间已耗尽
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    // 获取任务的回调函数
    const callback = currentTask.callback;
    if (typeof callback === "function") {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // 回调是不是已经过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        markTaskRun(currentTask, currentTime);
      } // 执行任务,并返回任务是否中断还是已执行完成
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime(); // 如果callback执行之后的返回类型是function类型就把又赋值给currentTask.callback,说明没执行完。没有执行完就不会执行pop逻辑,下一次返回的还是当前任务
      if (typeof continuationCallback === "function") {
        currentTask.callback = continuationCallback;
      } else {
        // 不是函数说明当前任务执行完,弹出来就行
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    // 取出下一个任务
    currentTask = peek(taskQueue);
  } // 如果task队列没有清空, 返回true。 等待Scheduler调度下一次回调
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    // task队列已经清空, 返回false.
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

可以理解为,Scheduler 的任务调度循环控制的颗粒度为任务层面

reconciler 中的工作循环

细心的同学可能发现了,上述任务调度循环中提到“完成某个任务后,会检测本轮任务调度所花费的时间”,那么如果有一个非常庞大的任务,它的执行时间远超 5ms,那么如果 React 也需等待它执行完毕后才能进行判断,从而让出主线程,那么此任务的执行过程也会使 React 应用处于长时间的阻塞。

上述提到的隐患的确是存在的,为了避免单个任务执行时间过长,从而阻塞主线程,React 除了上述提到的任务调度循环,还设计了另一个颗粒度更细的循环机制加以辅助——Fiber构建循环

Fiber 构建循环存在于 react-reconciler 包中,而不是 Scheduler 包中,因为 Fiber 工作单元的执行属于协调过程。

上面我们提到了,React 借助 Fiber 架构,将一整个渲染任务拆分成多个工作单元(即 Fiber 节点),每个工作单元的执行过程就是 Reconciler 构建 workInProgress 树的过程。当某个任务中的所有工作单元执行完成之后,那么此任务也就执行完成了。

同样地,在 Fiber 构建的过程中,每执行完一个工作单元,就会调用 shouldYieldToHost(代码中导入时会重命名为 shouldYield)判断时间片是否超时,如没超时则继续执行下一个工作单元,否则将会中断当前任务,让出主线程。且得益于 Fiber 架构的链式树状结构,在下次任务恢复时,可从中断的工作单元处恢复执行,而无需重新执行整个任务。

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    // performUnitOfWork就是Fiber节点构建的过程,包含beginWork和completeWork
    performUnitOfWork(workInProgress);
  }
}

上面源码中的 performUnitOfWork 函数即是 fiber 工作单元的执行函数,workInProgress 记录着当前需要执行的 fiber 节点,如 workInProgress 的值为 null,则证明当前任务的所有工作单元都已执行完毕。

双工作循环总结

上述 Scheduler 和 Reconciler 中的两个工作循环,分别从任务层面fiber 工作单元层面进行控制,使得 React 应用在执行渲染任务的过程中,能够及时主动地让出主线程,响应其他事件。

它们的关系如下图所示:

image.png

至此,React 通过双工作循环与时间切片配合,已经解决了同步执行渲染任务导致主线程无法响应用户事件的问题了。

高优先级任务打断低优先级任务

根据优先级打断的必要性

通过上面提到的双工作循环加时间切片,已经实现了渲染任务的异步执行,每隔大约 5ms 会让出主线程,去处理用户交互等事件。但要真正做到优化用户体验,似乎仅仅实现任务异步执行是不足够的,试想一下如下场景。

假设现在我们的任务队列中有多个任务,现在按按照上述双工作循环异步执行,而此时用户触发了点击事件,导致 React 应用的状态发生了改变,从而触发了一个新的渲染任务,并放置到任务队列的末端。那么按照上述逻辑,这个新的渲染任务需要等待前面的所有渲染任务执行完毕之后,才会执行,那么从用户点击到任务执行之间就存在较长的等待时间,用户可能会认为这是一个不好的体验。

上述场景的问题在于,任务队列中的任务没有优先级的概念,遵循先进先出的规则。那么像用户交互而触发的状态变化引起的渲染任务,可能需要等待较长时间才能执行,从而导致体验不佳。

为了优化上述场景,React 将不同情况引起的状态改变而触发的渲染任务分为不同的优先级(即上面提到的 Lanes 优先级模型),并且在低优先级任务执行期间,如果触发了高优先级任务,则高优先级任务可以“打断”低优先级任务,先行执行。

根据优先级打断的原理

上提到的“高优先级任务打断低优先级任务”的描述其实并不严谨。

实际上,当执行低优先级任务时,如果触发了高优先级任务,那么高优先级任务并不会主动打断低优先级任务的执行。而是任务调度循环基于时间片打断低优先级任务,然后在下一次任务调度的时候,在任务队列中取出优先级最高的任务执行。但由于 5ms 的时间片很短暂,所以造成高优先级任务主动打断低优先级任务的错觉。

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);

  // 通过小顶堆获取第一个优先级最高的任务
  currentTask = peek(taskQueue);

  // ...some code...
  // 循环执行任务队列中的任务,直至时间片耗尽
}

总结

为了优化用户体验,React 设计了并发的任务管理策略,实现了以下两个目标:

  • 在任务执行过程中,主动让出主线程,响应其他事件。
  • 在执行低优先级任务过程中,如触发了高优先级任务,可通过调度策略,优先执行高优先级任务。

实现的逻辑主要集中在时间切片 + 双工作循环 + 优先级系统:

  • 多个任务以时间片为单位执行,而非同步地一次性执行,每消耗完一个时间片,让出主线程。
  • 通过任务调度循环和 Fiber 构建循环,从任务层面和 Fiber 工作单元层面分别检测时间片,以更小颗粒度进行任务调度。
  • 为每个任务分配优先级,在每个时间片进行任务调度时,总是取出最高优先级的任务执行,以便及时响应高优先级任务。

参考

React源码 - 关键数据结构

React 中的数据结构

这篇文章将着重介绍 React 中的几个重要的数据结构以及设计思想。

Fiber 树

这里仅对 Fiber 进行一个简单的介绍,如想了解 Fiber 架构的细节,包括双缓冲树,可以阅读该源码系列的另一篇文章:React源码 - 大名鼎鼎的Fiber

Fiber 树可以说是 React 内部最重要的一种数据结构了,它帮助 React 实现了可中断渲染以及优先级调度。它拥有 childsiblingreturn 三个指针,分别表示:

  • child: 第一个子节点
  • sibling: 串联兄弟节点
  • return: 父节点

下面是 Fiber 节点构造函数的源码:

// facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.js#L136-L209
function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // fiber 类型
  this.key = key; // 用于调和子节点
  this.elementType = null; 
  this.type = null; // 元素类型
  this.stateNode = null; // 对应的真实 DOM 元素

  // Fiber 链表结构 
  this.return = null; // 指向父节点(父节点)
  this.child = null;  // 指向第一个子节点(子节点)
  this.sibling = null; // 指向下一个兄弟节点(兄弟节点)
  this.index = 0;     // 在父节点的子节点列表中的索引位置

  this.ref = null;
  this.refCleanup = null;

 // Props 和 State
  this.pendingProps = pendingProps; // 新一轮渲染中传入的新 props
  this.memoizedProps = null;        // 上一次渲染时使用的 props
  this.updateQueue = null;          // 状态更新队列,存储 setState 产生的更新对象
  this.memoizedState = null;        // 上一次渲染时使用的 state
  this.dependencies = null;         // 当前 Fiber 所依赖的上下文(Context)、事件订阅等
  
  this.mode = mode;

  // Effects
  this.flags = NoFlags;         // 当前 Fiber 需要执行的副作用(如 Placement, Update, Deletion)
  this.subtreeFlags = NoFlags;  // 子节点树中需要执行的副作用(用于性能优化)
  this.deletions = null;        // 待删除的子 Fiber 节点数组(用于记录需要被删除的节点)

  // Lane 模型(优先级调度) 
  // React 17+ 使用的优先级调度模型,用于并发渲染
  this.lanes = NoLanes;        // 当前 Fiber 上待处理的更新优先级车道
  this.childLanes = NoLanes;   // 子节点树中待处理的更新优先级车道

  // 双缓存技术
  this.alternate = null; // 指向 current 树或 workInProgress 树中的对应 Fiber 节点
                         // 用于实现双缓存机制,在更新时交替使用两棵树
}

Fiber 节点通过自身的 childsibling 以及 return 指针构建了一个树结构。并通过自身的 alternate 指针,指向 current treeworkInProgress tree 中的对应 Fiber 节点, 实现了如下图所示的双缓冲树的架构。

image.png

Hook 链表

在React中,Hook 是一组特殊的函数,让你在函数组件中应用 React 的特性(如状态、生命周期、上下文等)。

我相信很多同学都知道 React Hooks 不能在条件语句中调用。具体原因就是,它采用了链表的结构。每个函数组件在 Fiber 节点对象上都有一个 hooks 链表(Fiber.memoizedState

首次渲染时,React 会为每个调用的 hook 创建一个 Hook 对象,并用通过 next 指针串起来。之后的更新渲染,React 并不会“看代码里的变量名”,而是严格按调用顺序一个个取 Hook 对象。

// [ReactFiberHooks.js - facebook/react - GitHub1s](https://github1s.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L195-L201)
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};

image.png

Update Queue

从前文介绍的 Hook 定义中来看,Hook对象共有 5 个属性。 接下来介绍其中的 hook.queue 属性,对应的类型是 UpdateQueue

hook.queue 是每个 Hook(useState/useReducer)用于保存待处理状态更新的队列结构。它记录了所有等待执行的 setState()dispatch() 调用,React 会在下一次渲染时从这个队列中取出所有更新并计算出新的 state。

在 React Hook 实现中,hook.queue.pending 是一个环形链表,每个环节点是一个 update 对象(描述一次 state 更新)。需要注意的是queue.pending 指向最后一个节点, 而 queue.pending.next 才是第一个节点。

读 React 源码时容易困惑的一点是 React Hook 里的 queue 究竟是链表还是队列? 因为从UpdateQueue的命名上来看,可以翻译为更新队列,并且符合队列先进先出(FIFO)的特性。但从源码实现上看,它是一个环形链表。因为,UpdateQueue.pending 中的每个 Update 节点是通过 next 指针连接的。所以可以说,React 用 链表结构 实现了一个 队列行为

注意区分 fiber.updateQueuehook.queue。它们虽然都是环形链表,都负责状态更新的存储与合并, 但它们是两套 不同的机制,分别服务于 class componentfunction component。上面介绍的是 hook.queue

下面为 hook.queue 相关的类型定义源码:

// [ReactFiberHooks.js - facebook/react - GitHub1s](https://github1s.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L165-L181)
export type Update<S, A> = {
  lane: Lane,
  revertLane: Lane,
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
  gesture: null | ScheduledGesture, // enableGestureTransition
};

export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};

image.png

当调用:

setCount(c => c + 1);

React 内部会创建一个 update 对象:

const update = {
  action: c => c + 1,
  next: null,
};

接下来把该更新对象(update)加入到当前 hook 的更新队列中:

  • 每个 hook(比如 useState)都有一个 queue
  • queue.pending 存的是一个 环形链表(circular linked list),保存所有待处理的更新(update 对象)。
const pending = hook.queue.pending;
if (pending === null) {
  update.next = update; // 第一个节点,自己形成环
} else {
  update.next = pending.next;
  pending.next = update;
}
hook.queue.pending = update; // 更新尾节点

在下一次 render 阶段,遍历并执行一个 hook(useState/useReducer)上积累的所有更新(update 对象),计算出新的 state 值:

function processUpdateQueue(hook, queue, reducer) {
  let newState = hook.memoizedState;
  let pending = queue.pending;
  if (pending !== null) {
    const first = pending.next; // 环起点
    let update = first;
    do {
      newState = reducer(newState, update.action);
      update = update.next;
    } while (update !== first);
    queue.pending = null; // 所有更新处理完毕后清空
  }
  hook.memoizedState = newState;
}

小结

React 在内部通过一系列精妙的数据结构,将复杂的渲染流程拆解为可控的节点级更新。

  • Fiber 树 是整个架构的核心,它以链表形式组织组件节点,实现了可中断、可恢复的渲染能力,并通过 alternate 指针构建出高效的 双缓冲树(Double Buffer Tree)
  • Hook 链表 则以调用顺序为唯一依据,为函数组件的状态管理提供了结构化存储,使得每个 useStateuseEffect 等 Hook 都能在多次渲染中保持独立且稳定的状态。
  • Update Queue(更新队列) 采用环形链表的形式,在结构上实现了队列的先进先出(FIFO)行为。它承担了状态变更的暂存与合并,使 React 能在下一次渲染时批量处理多次 setState 调用,从而提升性能与一致性。

这些数据结构共同构成了 React 高性能、可中断渲染背后的基础设施。
理解它们,不仅能帮助我们更好地读懂源码,也能在实际项目中写出更高效、更符合 React 思想的代码。

浅谈React19的破坏性更新

2024年12月,React 19 正式发布,至今已过去大半年。尽管目前多数项目仍在使用 React 18,但我们可以通过官方文档和 GitHub 了解其带来的关键新特性和破坏性变更。以下是我总结的一些重要更新。

React相关文档也可参考这个地址:传送门

1、Optimistic (直译为:乐观)

React19新加入了一个概念,叫做乐观。对应的API为useOptimistic。乐观更新是一种 UI 设计模式,其核心思想是:

“先相信操作会成功,提前更新 UI;如果失败了,再回滚到之前的状态。 这与传统的“悲观更新”(先等待服务器响应,成功后再更新 UI)形成对比。

为什么需要乐观更新?
  • 提升用户体验:用户点击按钮后,UI 立即响应,无需等待网络延迟。
  • 感觉更快:即使网络慢,用户也能看到操作“已生效”,减少等待焦虑。
  • 现代应用的标准做法:如社交媒体点赞、评论删除等,都采用此模式。

示例

import { useOptimistic, useState } from "react";
import { postComment as apiPostComment } from "../utils";

const apiPostComment = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('comment success')
    }, 1000)
  })
}

// 评论项组件
function CommentItem({ comment }) {
  return (
    <div className="border p-3 mb-2 rounded bg-white shadow-sm">
      <p className="text-gray-800">{comment.text}</p>
      {comment.isPending && (
        <small className="text-blue-500 mt-1 block">正在提交...</small>
      )}
    </div>
  );
}

// 评论区域组件
function CommentSection({ commentList, onAddComment }) {
  const [pendingCommentId, setPendingCommentId] = useState(0);

  // 乐观更新:立即显示待定评论
  const [displayedComments, addPendingComment] = useOptimistic(
    commentList,
    (state, newCommentText) => {
      const id = `pending-${pendingCommentId}`;
      return [
        ...state,
        {
          id,
          text: newCommentText,
          isPending: true,
        },
      ];
    }
  );

  // 表单提交处理器
  async function handleAddComment(formData) {
    const content = formData.get("commentContent");
    if (!content?.trim()) return;

    // 生成本次提交的临时 ID
    const currentPendingId = pendingCommentId;
    setPendingCommentId((prev) => prev + 1);

    // 1. 立即乐观更新 UI
    addPendingComment(content);

    try {
      // 2. 发起真实请求
      const result = await onAddComment(formData, `pending-${currentPendingId}`);

      if (result.error) {
        throw new Error(result.error.message);
      }
    } catch (error) {
      console.error("评论提交失败:", error.message);
      // 注意:useOptimistic 不会自动回滚,需配合其他状态管理
    }
  }

  return (
    <div className="comment-container">
      {/* 渲染当前显示的评论(含乐观更新) */}
      {displayedComments.length === 0 ? (
        <p className="text-gray-500 italic">暂无评论</p>
      ) : (
        displayedComments.map((comment) => (
          <CommentItem key={comment.id} comment={comment} />
        ))
      )}

      {/* 添加评论表单 */}
      <form onSubmit={handleAddComment} className="mt-4 space-y-3">
        <textarea
          name="commentContent"
          placeholder="写下你的评论..."
          rows="3"
          className="w-full border rounded p-2 focus:outline-none focus:ring-2 focus:ring-blue-300"
          autoComplete="off"
        />
        <button
          type="submit"
          className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded transition"
        >
          发布评论
        </button>
      </form>
    </div>
  );
}

// 页面组件:评论功能演示
export default function OptimisticCommentDemo() {
  const [errorMessage, setErrorMessage] = useState("");
  const [comments, setComments] = useState([
    { id: "initial-1", text: "第一条评论!", isPending: false },
  ]);

  // 处理真实评论提交
  async function handleCommentSubmit(formData, pendingId) {
    const content = formData.get("commentContent");

    // 模拟 API 调用
    const response = await apiPostComment({ content });

    if (response.error) {
      setErrorMessage(`"${content}" 提交失败:${response.error.message}`);
      return response;
    } else {
      setErrorMessage("");
      // ✅ 成功后:添加真实评论(不带 isPending)
      setComments((prev) => [
        ...prev,
        {
          id: `comment-${Date.now()}`,
          text: response.data.text,
          isPending: false,
        },
      ]);
    }

    return response;
  }

  return (
    <div className="p-6 max-w-2xl mx-auto">
      <h2 className="text-2xl font-bold text-gray-800 mb-4">
        乐观更新评论系统演示
      </h2>

      <CommentSection commentList={comments} onAddComment={handleCommentSubmit} />

      {errorMessage && (
        <p className="text-red-600 mt-4 text-sm bg-red-50 p-3 rounded">
          {errorMessage}
        </p>
      )}
    </div>
  );
}

2、use (支持异步)

1、use可读取promise

示例:

import { useState, use, Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

// 模拟异步获取天气数据
function fetchWeatherData(city) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 50% 概率失败,增加错误边界演示效果
      if (Math.random() < 0.5) {
        reject(new Error("天气服务暂时不可用"));
      } else {
        resolve({
          city,
          temperature: Math.round(Math.random() * 30),
          condition: ["晴", "多云", "小雨", "雷阵雨"][Math.floor(Math.random() * 4)],
          lastUpdated: new Date().toLocaleTimeString(),
        });
      }
    }, 1200);
  });
}

export default function WeatherDashboard() {
  const [weatherQuery, setWeatherQuery] = useState(null);
  const [isFetching, setFetching] = useState(false);

  const handleFetchWeather = () => {
    setWeatherQuery(fetchWeatherData("杭州"));
    setFetching(true);
  };

  if (!isFetching) {
    return (
      <div className="text-center">
        <h2 className="text-xl font-semibold mb-4">🌤️ 天气信息看板</h2>
        <button
          onClick={handleFetchWeather}
          className="bg-green-500 hover:bg-green-600 text-white px-5 py-2 rounded transition"
        >
          获取杭州天气
        </button>
      </div>
    );
  }

  return <WeatherDisplay weatherPromise={weatherQuery} />;
}

// 展示天气数据的容器(含错误和加载状态)
function WeatherDisplay({ weatherPromise }) {
  console.log(
    "%c [ WeatherDisplay ]-38",
    "font-size:13px; background:#4B5563; color:#fff; padding:2px 6px;",
    "渲染 WeatherDisplay"
  );

  return (
    <div className="max-w-md mx-auto p-4 border rounded-lg shadow bg-white">
      <h3 className="text-lg font-medium text-gray-800 mb-3">🌤️ 实时天气</h3>

      <ErrorBoundary fallback={<WeatherError />}>
        <Suspense fallback={<WeatherLoading />}>
          <WeatherCard dataPromise={weatherPromise} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

// 加载中状态
function WeatherLoading() {
  return (
    <div className="text-center text-gray-500 animate-pulse">
      <p>📡 正在连接天气服务器...</p>
      <p className="text-sm mt-1">请稍候</p>
    </div>
  );
}

// 错误状态
function WeatherError() {
  return (
    <div className="text-center text-red-600">
      <p>❌ 获取天气失败</p>
      <p className="text-sm mt-1">请检查网络或稍后重试</p>
    </div>
  );
}

// 实际渲染天气数据(使用 use)
function WeatherCard({ dataPromise }) {
  const weather = use(dataPromise);

  return (
    <div>
      <p><strong>城市:</strong>{weather.city}</p>
      <p><strong>温度:</strong>{weather.temperature}°C</p>
      <p><strong>天气:</strong>{weather.condition}</p>
      <p className="text-xs text-gray-500 mt-2">
        更新时间:{weather.lastUpdated}
      </p>
    </div>
  );
}

2、use可读取context

示例:

import {use} from 'react';
import ThemeContext from './ThemeContext'

function Heading({children}) {
  if (children == null) {
    return null;
  }
  
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );

3、Ref 支持在 props 中转发使用

从19开始,ref可以作为prop在函数组件中使用了。之前函数组件中想要使用ref,必须使用forwardRef。这表示着从React19开始,forwardRef可能要被弃用了。会对一些三方组件库有比较大的影响,因为基本上每个组件都会用到ref,之前的写法都是使用的forwardRef

1、React19给ref加上了自己的清理函数,之前可以用useEffect清理,现在ref有自己的清理函数了。

示例:

<div
  ref={(ref) => {
    // 当元素从DOM中移除时的引用。
    return () => {
      // ref 清理函数
    };
  }}
/>

4、Context 改动

之前使用 Context 基本就是三步走:创建Context;Provider传递value;后代组件消费value。React19中简化了Context.Provider,可以直接使用Context代替Provider:

示例:

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );  
}

5、其他改动

1、支持文档元数据

示例:

export default function Demo ({ props }) {
  return (
    <section>
      <title>{props.title}</title>
      <meta name="name" content="content" />
      <Link rel="test" href="xxx" />
      <meta name="keywords" content={props.keywords} />
      <p>测试内容...</p>
    </section>
  )
}

2、支持样式表

export default function Demo ({ props }) {
  return (
    <section>
      <link rel="stylesheet" href="xxx" />
      <p>测试内容...</p>
    </section>
  )
}

3、支持异步脚本

export default function Demo ({ props }) {
  return (
    <section>
      <script async={true} src="xxx" />
      <p>测试内容...</p>
    </section>
  )
}

6、支持自定义元素

在之前的版本中,React会把不认识的props当做attributes来处理,在React19中,此行为改为与自定义元素实例上的属性匹配的 props 被分配为 properties,其他的被分配为attributes。

React 第四十四节Router中 usefetcher的使用详解及注意事项

前言

useFetcherReact Router 中一个强大的钩子,用于在不触发页面导航的情况下执行数据加载(GET)或提交(POST)

一、useFetcher 应用场景:

1、后台数据预加载(如鼠标悬停时加载数据) 2、无刷新表单提交(如点赞、评论) 3、多步骤表单的局部提交 4、与当前页面路由解耦的独立数据操作

二、useFetcher 核心特性

非侵入式操作:不会影响当前路由或 URL。 状态跟踪:提供 state 属性跟踪操作状态(idle/submitting/loading)数据缓存:自动管理相同请求的缓存,避免重复提交多实例支持:可在同一页面多次使用,独立管理不同操作。

三、useFetcher 基本使用

3.1、 初始化 Fetcher

import { useFetcher } from "react-router-dom";

function LikeButton() {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method="post" action="/api/like">
      <button type="submit">
        {fetcher.state === "submitting" ? "点赞中..." : "点赞"}
      </button>
    </fetcher.Form>
  );
}

3.2、useFetcher核心 API 与参数

useFetcher() 返回一个对象,包含以下属性和方法:

属性/方法 Form:React 组件用于 替代 <form>,提交时不会触发页面导航 submit(data, options):函数用于 手动提交数据(支持 FormData/对象/URL参数) load(url):函数用于 手动触发 GET 请求加载数据 data:any表示 最近一次成功操作返回的数据 state"idle"/"submitting"/"loading"表示 当前操作状态

四、useFetcher 完整案例:用户评论功能

4.1、 组件代码

function CommentSection({ postId }) {
  const fetcher = useFetcher();
  const [commentText, setCommentText] = useState("");

  // 显示提交后的评论(包括乐观更新)
  const comments = fetcher.data?.comments || [];

  return (
    <div>
      <h3>评论列表</h3>
      <ul>
        {comments.map((comment) => (
          <li key={comment.id}>{comment.text}</li>
        ))}
      </ul>

      <fetcher.Form
        method="post"
        action={`/posts/${postId}/comment`}
        onSubmit={() => setCommentText("")} // 清空输入框
      >
        <textarea
          name="text"
          value={commentText}
          onChange={(e) => setCommentText(e.target.value)}
        />
        <button type="submit" disabled={fetcher.state !== "idle"}>
          {fetcher.state === "submitting" ? "提交中..." : "发布评论"}
        </button>
      </fetcher.Form>

      {/* 显示错误信息 */}
      {fetcher.data?.error && (
        <div className="error">{fetcher.data.error}</div>
      )}
    </div>
  );
}

4.2、 后端路由处理(示例)

// 路由配置中的 action 函数
export async function commentAction({ request, params }) {
  const postId = params.postId;
  const formData = await request.formData();
  
  try {
    // 模拟 API 调用
    const response = await fetch(`/api/posts/${postId}/comment`, {
      method: "POST",
      body: JSON.stringify({ text: formData.get("text") }),
    });
    
    if (!response.ok) throw new Error("评论失败");
    const result = await response.json();
    
    // 返回更新后的评论列表
    return { comments: result.comments };
  } catch (error) {
    return { error: error.message };
  }
}

五、useFetcher 高级用法:手动控制数据流

5.1、手动提交数据

function SearchBox() {
  const fetcher = useFetcher();
  const [query, setQuery] = useState("");

  // 输入变化时自动搜索(防抖)
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      if (query) {
        fetcher.load(`/api/search?q=${query}`);
      }
    }, 300);

    return () => clearTimeout(timeoutId);
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      {/* 显示搜索结果 */}
      {fetcher.data?.results?.map((result) => (
        <div key={result.id}>{result.name}</div>
      ))}
    </div>
  );
}

5.2、处理文件上传

function AvatarUpload() {
  const fetcher = useFetcher();

  const handleFileChange = (e) => {
    const file = e.target.files[0];
    const formData = new FormData();
    formData.append("avatar", file);

    // 手动提交 FormData
    fetcher.submit(formData, {
      method: "post",
      action: "/api/upload-avatar",
      encType: "multipart/form-data",
    });
  };

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      {fetcher.data?.url && (
        <img src={fetcher.data.url} alt="用户头像" />
      )}
    </div>
  );
}

六、useFetcher使用注意事项

6.1、路由配置要求

必须使用数据路由(通过 createBrowserRouter 创建路由)

提交的目标路由(action 路径)需要定义对应的 action 函数

6.2、性能优化

避免高频调用 load/submit(如搜索框需防抖)

对相同 URL 的请求,React Router自动去重

6.3、错误处理

通过 fetcher.data 接收 action 返回的错误信息

使用 try/catch 包裹异步操作并返回错误状态

6.4、与全局状态配合

如果需要更新全局数据(如用户信息),结合 useRevalidator() 重新验证路由加载器

七、useFetcher与普通表单提交的对比

在这里插入图片描述

总结:

我们可以用useFetcher 实现高度交互的 Web 应用,同时保持代码的简洁性可维护性。 它是构建现代 SPA(单页面应用) 中复杂交互(如即时保存、实时搜索)的理想工具。

如有错误之处,欢迎评论指正

React 19.2 重磅更新:终于解决 useEffect 依赖数组难题

useEffect 的时候,依赖数组总让人头疼——函数放进去吧,effect 频繁重跑;不放进去吧,又拿到陈旧的闭包值。Tab 切换想保留状态,用 CSS display: none 又感觉绕了个弯。

React 19.2 带来的 <Activity />useEffectEvent 就是专门解决这些问题的。

先抛几个问题,看看你是不是也曾被它们困扰:

  • useEffect 时,是不是总在为依赖数组烦恼?为了避免重复执行,把函数提到外面,结果又遇到了闭包陷阱。
  • 标签页(Tabs)切换时,如何能在保留组件状态的同时,优雅地隐藏和显示内容?用 CSS display: none 总感觉不够“React”。
  • 服务器渲染(SSR)的页面,如果组件嵌套得深,是不是会看到页面一块一块地加载,体验不太好?
  • useEffect 里的事件监听函数(比如 onClick),是不是必须放进依赖数组里,导致 effect 频繁重新执行?(这个最容易搞混)

<Activity /> API:声明式管理组件状态

之前实现 Tab 切换保留状态,通常是用 CSS 控制 displayvisibility。能用,但总感觉是在"绕路"。

<Activity /> 组件提供了一种声明式的、更符合 React 思想的解决方案。

核心功能

<Activity /> 通过 mode prop 控制子组件的活动状态:

  • mode="visible" - 挂载并显示
  • mode="hidden" - 暂停渲染,但状态和 DOM 树保留,不会卸载

适用场景:模态框、Tab、后台预渲染的 UI。

实战案例:带状态的 Tabs 组件

常见场景:每个 Tab 都有自己的内部状态(比如计数器),切换时要保留。

import { useState } from 'react';
import { Activity } from 'react'; // 从 react 导入

function CounterTab({ title }) {
  const [count, setCount] = useState(0);
  console.log(`${title} rendered`);
  return (
    <div>
      <h3>{title}</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

export default function App() {
  const [activeTab, setActiveTab] = useState('A');

  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab('A')}>Tab A</button>
        <button onClick={() => setActiveTab('B')}>Tab B</button>
      </nav>
      <hr />
      <Activity mode={activeTab === 'A' ? 'visible' : 'hidden'}>
        <CounterTab title="Tab A" />
      </Activity>
      <Activity mode={activeTab === 'B' ? 'visible' : 'hidden'}>
        <CounterTab title="Tab B" />
      </Activity>
    </div>
  );
}

切换 Tab 时会发现:

  1. 每个 Tab 的 console.log 只在首次渲染时打印
  2. 切换回来后,count 状态完美保留

比 CSS display: none 优雅,由 React 调度器管理,未来可能结合 Offscreen API 带来更多性能优势。


useEffectEvent:解决依赖数组难题

useEffect 的依赖数组是核心,也是 Bug 高发区。想在 effect 里调用总能获取最新 props/state 的函数,又不想因函数引用变化导致 effect 重跑,这个矛盾困扰了很多人。

useEffectEvent 就是来解决这个问题的。

之前的痛点

经典问题:记录页面访问的 useEffect,依赖 user 对象。

function Page({ user, url }) {
  useEffect(() => {
    // 问题:如果 user 对象在每次渲染时都是新对象,
    // 这个 effect 会在每次 user 变化时都重新执行,
    // 即使我们只关心 url 变化时才记录日志。
    logVisit(user, url);
  }, [user, url]); // 依赖数组很烦人
}

为了避免 user 变化导致 effect 重跑,把它从依赖数组去掉,就会产生"陈旧闭包"——logVisit 捕获的是第一次渲染的 user,后续更新拿不到最新值。

useEffectEvent 的解决方案

useEffectEvent 可以将一个函数“提升”到 React 的管理体系中,让它不参与到 useEffect 的依赖追踪里,但总能访问到最新的 props 和 state。

import { useEffect, useEffectEvent } from 'react';

function Page({ user, url }) {
  // 1. 用 useEffectEvent 包裹你的事件函数
  const onVisit = useEffectEvent(visitedUrl => {
    // 这里的 user 总能读到最新的值
    logVisit(user, visitedUrl);
  });

  // 2. 在 useEffect 中调用它
  useEffect(() => {
    // onVisit 函数的引用是稳定的,不需要加入依赖数组
    // 现在这个 effect 只会在 url 变化时执行
    onVisit(url);
  }, [url]); // 依赖数组清爽多了!
}

核心要点useEffectEvent 分离了"响应式逻辑"(应该触发 Effect 的,如 url)和"非响应式逻辑"(不应触发的,如 user),让 useEffect 的意图更清晰。


SSR 性能优化:Suspense 边界批处理

SSR 时,如果页面有多个 <Suspense> 边界,19.2 之前会为每个边界都发送一个 fallback HTML,然后再发送完整内容,导致网络请求碎片化。

19.2 引入批处理机制:React 在服务器上稍等片刻,将多个准备就绪的 Suspense 边界内容合并到同一个 HTML 响应,减少网络往返,提升初始加载性能。

graph TD
    subgraph Before_React_19_2 ["Before (React < 19.2)"]
        A[Request] --> B{SSR}
        B --> C1[Shell + Fallback 1]
        B --> C2[Content 1]
        B --> C3[Shell + Fallback 2]
        B --> C4[Content 2]
    end

    subgraph After_React_19_2 ["After (React 19.2+)"]
        X[Request] --> Y{SSR}
        Y --> Z[Shell + Content 1 + Content 2]
    end

这个优化自动开启,无需配置。


其他更新

  • cacheSignal - 专为 Server Components 设计,当 cache() 数据过期时提供 AbortSignal,可中断异步操作避免资源浪费
  • 部分预渲染 (PPR) - 预渲染静态"外壳",然后动态填充内容,19.2 完善了 API
  • DevTools 性能追踪 - Chrome DevTools Performance 面板新增 React 专属轨道,可视化组件挂载、更新和 Effect 执行

如何升级?

# 使用 npm
npm install react@latest react-dom@latest

# 使用 pnpm
pnpm update react --latest
pnpm update react-dom --latest

总结

React 19.2 核心要点:

实用层面

  • <Activity /> - 状态保留场景的官方解决方案
  • useEffectEvent - 重构复杂 useEffect 的利器,提升代码可读性和稳定性
  • SSR 性能自动优化,无需配置

官方文档:

react.dev/blog/2025/1…

构建可维护的 React 应用:系统化思考 State 的分类与管理

构建可维护的 React 应用:系统化思考 State 的分类与管理

在 React 开发中,我们常说“状态是应用的命脉”。然而,对于如何组织和管理这些状态,许多开发者,尤其是新手,往往停留在“能用就行”的层面,习惯于将所有状态都塞进 useState 这个“万能”钩子中。这就像把所有的文件——无论是合同、照片、还是临时笔记——都杂乱地扔在同一个桌面上,短期内似乎方便,但随着项目复杂度的提升,寻找、修改和维护都会变得异常困难。

真正的解决方案来自于架构层的系统化思考:根据状态的来源、作用域和生命周期,对其进行清晰的分类,并为每一类选择最合适的管理工具。本文将状态系统地划分为四类:Server State、全局 Client State、本地组件 State 和 URL State,并深入探讨其管理策略。

为什么状态分类是架构的基石?

无差别的状态管理会导致:

  1. 组件过度耦合:状态散落在各处,难以追踪和调试。
  2. 性能瓶颈:不必要的重渲染,因为一个状态的更新可能会触发整个组件树的刷新。
  3. 数据不一致:特别是服务端状态,容易产生陈旧数据。
  4. 可测试性差:状态逻辑与 UI 耦合,难以进行单元测试。

通过分类,我们实现了 “关注点分离” ,让每种状态各司其职,从而构建出更清晰、更健壮、更易扩展的应用程序架构。


State 的四大分类与管理策略

1. Server State(服务端状态)

定义:从后端服务器获取的数据,如用户列表、商品信息、博文内容等。

特点

  • 所有权不属于前端,前端只是缓存和同步。
  • 可能存在多个副本(多个组件使用同一数据)。
  • 需要处理缓存、更新、失效、后台同步等复杂问题。
  • 需要处理加载和错误状态

错误示范:使用 useStateuseEffect 获取数据后,将数据保存在组件的本地状态中。这会导致:

  • 重复请求:多个组件需要同一数据时,会发起多个相同请求。
  • 陈旧数据:数据在其他地方更新后,当前组件无法感知。
  • 缺乏缓存:组件卸载后重新挂载,需要重新请求。

推荐管理工具React Query, SWR, Apollo Client

🌰(使用 React Query):

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 自动处理 loading, error, 缓存和数据更新
function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId], // 唯一的缓存键
    queryFn: () => fetchUser(userId), // 获取数据的函数
  });

  const queryClient = useQueryClient();
  const updateUserMutation = useMutation({
    mutationFn: updateUser,
    onSuccess: () => {
      // 更新成功后,使旧的缓存失效,触发重新获取
      queryClient.invalidateQueries(['user', userId]);
    },
  });

  if (isLoading) return 'Loading...';
  if (error) return 'An error occurred';

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => updateUserMutation.mutate({ ...user, name: 'New Name' })}>
        Update Name
      </button>
    </div>
  );
}

优点

  • 自动化缓存:避免不必要的网络请求。
  • 后台自动更新:可以在窗口重新聚焦时重新请求数据。
  • 乐观更新:先更新 UI,再发送请求,提升用户体验。
  • 内置加载和错误状态
  • 数据共享:不同组件使用相同 queryKey 会共享同一份缓存数据。

2. 全局 Client State(全局客户端状态)

定义:在多个不相关的组件间需要共享的、由前端自身产生的状态。例如:用户认证信息、主题、全局通知、多步表单的共享数据、购物车。

特点

  • 作用域是整个应用或大部分模块
  • 状态更新需要能够触发多个组件的响应式更新

错误示范:使用 useState 提升到顶层并通过 props 层层传递(“Prop Drilling”)。这会导致组件耦合过紧,中间组件被迫传递它们不关心的数据。

推荐管理工具Zustand, Redux Toolkit, Context API

🌰(使用 Zustand):

// stores/useThemeStore.js
import { create } from 'zustand';

const useThemeStore = create((set) => ({
  theme: 'light',
  toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}));

// ComponentA.js
function ComponentA() {
  const theme = useThemeStore((state) => state.theme);
  return <div>Current theme: {theme}</div>;
}

// ComponentB.js
function ComponentB() {
  const toggleTheme = useThemeStore((state) => state.toggleTheme);
  return <button onClick={toggleTheme}>Toggle Theme</button>;
}

优点

  • Zustand/Redux:状态与组件解耦,性能优化精细,支持中间件,开发工具强大。
  • Context API:React 原生,适合不频繁更新的简单全局状态(如 locale、主题)。对于频繁更新的复杂状态,需要手动优化以避免性能问题。

3. 本地组件 State(本地组件状态)

定义:完全属于单个组件或其直接子组件的临时状态。例如:一个输入框的值、一个下拉菜单的展开/收起状态、一个按钮的 loading 状态。

特点

  • 作用域严格限制在组件内部
  • 状态逻辑简单,生命周期与组件相同。

推荐管理工具useState, useReducer

🌰:

function LoginForm() {
  // 本地状态:输入框值和提交状态
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    // ... 提交逻辑
    setIsSubmitting(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={username} onChange={(e) => setUsername(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

优点

  • 简单直观:无需引入外部库,逻辑自包含。
  • 高内聚:状态和修改它的逻辑都封装在组件内部,易于理解和维护。

4. URL State(URL 状态)

定义:可以通过 URL 表示和共享的状态。例如:当前页面路由、查询参数、哈希值。

特点

  • 可分享:用户可以通过复制 URL 分享当前视图。
  • 可收藏:刷新页面后状态不丢失。
  • 与浏览器历史集成:支持前进/后退导航。

推荐管理工具React Router, Next.js Router 等路由库

🌰(使用 React Router):

import { useSearchParams, useParams } from 'react-router-dom';

function ProductList() {
  // 1. 管理查询参数:?category=books&sort=price
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category');
  const sort = searchParams.get('sort');

  const updateFilters = (newCategory, newSort) => {
    setSearchParams({ category: newCategory, sort: newSort });
  };

  // 2. 管理动态路由参数:/product/:productId
  const { productId } = useParams(); // 例如,URL 是 /product/123

  return (
    <div>
      <h1>Products in {category}</h1>
      <button onClick={() => updateFilters('electronics', 'name')}>
        Show Electronics
      </button>
      {/* 当 productId 存在时,显示产品详情 */}
      {productId && <ProductDetail id={productId} />}
    </div>
  );
}

优点

  • 状态持久化:刷新页面不丢失。
  • 极佳的用户体验:支持深度链接和浏览器导航。
  • 状态来源单一:URL 是许多 UI 状态的“唯一事实来源”。

总结与决策流程图

将状态正确地分类并选择相应的工具,是构建可扩展 React 应用架构的关键一步。

状态类型 特点 推荐工具 错误用法
Server State 来自后端,需缓存同步 React Query, SWR useState + useEffect
全局 Client State 跨组件共享 Zustand, Redux, Context Prop Drilling
本地组件 State 组件内部临时状态 useState, useReducer 过度使用全局状态
URL State 可分享、可收藏 React Router 用本地状态管理路由逻辑

当我们创建下一个状态时,可以先遵循以下决策流程思考一番:

  1. 这个状态是从服务器来的吗?
    • -> 考虑使用 React Query/SWR
  2. 这个状态需要在多个不相关的组件间共享吗?
    • -> 考虑使用 Zustand/Redux
  3. 这个状态是否定义了用户当前看到的界面(如页面、标签、筛选器),并且应该可以通过 URL 分享?
    • -> 考虑使用 URL State(路由库)
  4. 以上都不是?
    • -> 放心地使用 useStateuseReducer

通过这种系统化的思考方式,我们的代码库将不再是状态的“垃圾场”,而是一个条理清晰、职责分明、易于维护和扩展的现代化软件架构。

React 中的代数效应:从概念到 Fiber 架构的落地

参考:《React 技术揭秘》 by 卡颂

一、前言:React,不只是“快”

React 团队做架构升级,从来不是为了单纯的“更快”。
如果只是性能,他们完全可以优化 reconciliation 算法或者 diff 策略。

他们真正追求的,是**“控制时间”**——
让 UI 的更新可被中断、调度、恢复,就像一位懂分寸的画家,
知道什么时候该收笔,什么时候该补色。

这正是 React Fiber 想要实现的哲学。

而理解它的钥匙,藏在一个看似“学术味”的概念里:
👉 代数效应(Algebraic Effects)


二、代数效应:一个让函数更“有礼貌”的思想

简单来说,代数效应解决的是一个老大难问题:

当一个函数既要保持纯净逻辑,又要处理副作用时,该怎么办?

我们先看一个极简例子👇

function getTotalPicNum(user1, user2) {
  const picNum1 = getPicNum(user1);
  const picNum2 = getPicNum(user2);
  return picNum1 + picNum2;
}

逻辑简单到极致——加法而已。
但一旦 getPicNum 变成异步(比如去服务器查图片数),
整条函数调用链就被 async/await 感染了:

async function getTotalPicNum(user1, user2) {
  const picNum1 = await getPicNum(user1);
  const picNum2 = await getPicNum(user2);
  return picNum1 + picNum2;
}

于是,你的整个项目从同步世界坠入了“Promise 地狱”。
这就像一场小感冒引发了全公司的核酸检测。

代数效应的思路是这样的:

副作用由外部捕获和恢复,函数内部依然保持纯净。

为了说明,我们用一段虚构语法来模拟它的思想(不是 JS 代码,只是概念演示):

function getPicNum(name) {
  const picNum = perform name; // 执行副作用
  return picNum;
}

try {
  getTotalPicNum('kaSong', 'xiaoMing');
} handle (who) {
  switch (who) {
    case 'kaSong':
      resume with 230;
    case 'xiaoMing':
      resume with 122;
  }
}

这里的 perform 会触发外层的 handle
resume 再将结果带回中断点继续执行。

也就是说,函数逻辑和副作用的执行被“分离”了。
听起来是不是有点像 React 的 “render” 与 “commit” 阶段?


三、Fiber 登场:React 的代数效应工程实现

React Fiber 是 React 团队为了解决同步递归更新无法中断的问题而重写的协调器。

换句话说,它是 React 的一次“灵魂重构”。

Fiber 的核心目标是:

  • 支持任务分片与优先级调度
  • 允许任务中断与恢复
  • 恢复后能复用中间结果

或者更通俗点说:

以前 React 渲染是一口气吃完的火锅;
Fiber 让它可以夹一口肉,放下筷子接个电话,再回来继续吃。


🌿 Fiber 是什么?

Fiber(纤程)并不是 React 发明的词。
它早就出现在计算机领域中,与进程(Process)、线程(Thread)、协程(Coroutine)并列。

在 JavaScript 世界里,协程的实现是 Generator
所以我们可以说:

Fiber 是 React 在 JS 里,用链表结构模拟协程的一种实现。

简单理解:
Fiber 就是一种可中断、可恢复的执行模型。


四、Fiber 节点结构:链表串起的「可中断栈」

Fiber 架构中,每个 React Element 对应一个 Fiber 节点,
每个 Fiber 节点都是一个「工作单元」。

结构大致如下👇

FiberNode {
  type,          // 对应组件类型
  key,           // key
  return,        // 父 Fiber
  child,         // 第一个子 Fiber
  sibling,       // 兄弟 Fiber
  pendingProps,  // 即将更新的 props
  memoizedProps, // 已渲染的 props
  stateNode,     // 对应的 DOM 或 class 实例
}

它的核心是用链表结构,模拟函数调用栈
这样 React 就能“暂停栈帧”,在浏览器空闲时恢复执行。

想象一个任务循环(伪代码):

while (workInProgress && !shouldYield()) {
  workInProgress = performUnitOfWork(workInProgress);
}

shouldYield() 会检测浏览器时间是否用完。
如果主线程要去干别的事(比如动画、用户输入),React 会体贴地暂停。

这,就是 React Fiber 的“可中断渲染”精髓。


五、那 Generator 呢?为什么不用它?

有人会问:

“Generator 不也能暂停和恢复吗?为什么 React 要造轮子?”

确实,Generator 曾经是候选方案。
但它的问题在于两点:

  1. 传染性太强:用过 yield 的函数,整条调用链都得改。
  2. 上下文绑定太死:一旦被中断,就难以灵活恢复特定任务。

举个例子:

function* doWork(A, B, C) {
  var x = doExpensiveWorkA(A);
  yield;
  var y = x + doExpensiveWorkB(B);
  yield;
  var z = y + doExpensiveWorkC(C);
  return z;
}

如果任务中途被打断,或者来个更高优的任务插队,
xy 的计算状态就乱套了。

而 Fiber 则通过链表结构,把中间状态封装在节点上,
可以安全暂停、恢复、复用。


六、Fiber 调度:React 的“任务分片大师”

React Fiber 的渲染过程分两阶段:

  1. Render 阶段(可中断) :生成 Fiber 树,计算变更。
  2. Commit 阶段(不可中断) :执行 DOM 操作,提交更新。

示意图如下:

Render(可中断) ----> Commit(同步)
     ↑                    ↓
   调度器控制         应用更新

这正是代数效应的工程化体现:
逻辑阶段(Render)可以被中断和恢复,
副作用阶段(Commit)则由系统集中处理。


七、Hooks:代数效应的另一种体现

再看看 Hook:

function App() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

这里的 useStateuseReducer 等 Hook,
其实也是代数效应思想的体现:

组件逻辑中不关心状态保存的机制,只管声明“我需要一个状态”。

React 内部帮你处理 Fiber 节点中的状态链表。
你只需关心「逻辑」而非「调度」,
就像写同步代码一样,优雅得让人上瘾。


八、图解:Fiber = 可恢复的调用栈

App
 ├── Header
 │    └── Logo
 ├── Content
 │    ├── List
 │    └── Detail
 └── Footer

Fiber 的结构类似于:

  • child 指向第一个子节点;
  • sibling 指向兄弟节点;
  • return 指回父节点。

这让 React 能像遍历树一样遍历组件,
并随时暂停、恢复任务,而不丢上下文。


九、总结:React 的“时间魔法”

如果你把 React 比作魔术师,那 Fiber 就是它的魔杖。
它让 React 拥有了“操控时间”的能力:

  • 可以让任务暂停,等浏览器忙完再继续;
  • 可以按优先级执行任务(比如用户输入优先于动画);
  • 可以在恢复时复用中间状态,不浪费计算。

🎯 一句话总结:

React Fiber = 代数效应 + 调度器 + 状态复用


react项目开发—关于代码架构/规范探讨

社区一直讨论的一个主题,到底是react好,还是vue好?

我的答案

本人对这个问题的答案是这样的:

1、react给了我们更大的自由度,我们可以以任意的方式,组建我们的代码结构,我们可以操控的代码细节更多,也就能在更多的细节上面,对我们的代码进行更细致的优化。

2、react在书写的过程中,驱动我们对数据、UI有更清晰的认知。我们必须对他们的运行细节,ui变化,数据流向,有明确的认知边界,才能对我们的项目,进行清晰的掌控。

3、vue2,它是死板的,明确的定义了数据的申明在哪里,生命周期在哪里书写,函数和事件在哪里书写,我们几乎很少有可发挥的空间。

4、vue2,数据的双向绑定,让我们可以忽视数据内部真正的变化边界,我们需要的时候,直接无脑赋值就能得到我们想要的结果。

导致的结果

这是这两个框架,不同的api,给我们的客观印象。同时因为团队的差异,它们又导致了一些问题。

1、react框架的项目开发,它注重细节,注重每一个数据的驱动,这就导致了,用这个框架的前端团队必定要有一定的极客精神。它的过度自由,导致了我们甚至可以随意定义我们数据的管理规范、组件的划分、甚至任意的代码结构。

但是,这正是一个团队最可怕的东西。它导致了不可控。

2、vue2的项目开发,它是简单的,容易上手的,并且它就是一个固定的写法规范。

  • 它的vue文件,就是由标准的html、js、css三部分构成。
  • js里面,数据定义,生命周期,函数和事件,都有固定的地方。

这是vue2作为一个框架的缺点,代码可以控制的比react少,书写代码的细节上,性能可控性小。但是,作为团队视角,它提供了一个团队最重要的东西——相对简单、相对可控。

React项目,我们可以从哪些方面提升代码的水准

评价一个项目的代码水平到底好不好,有这么几个方面。

1、代码性能和质量。

代码性能和质量,有些是项目工程层面的内容,有些是代码实现方面的内容。

项目工程层面:代码包的体积大小、首次加载的效率、首屏渲染时间、用户可操作时间。

代码实现方面:渲染效率、是否卡顿、大数据量的处理、虚拟列表、图片加载、分块渲染等等。

2、代码整体的层级划分

目前,现阶段的大多数前端,能做到的基本的目录结构划分,也就是我们的src下面,有这么几个主体目录:

  • index.js/index.tsx,整个应用的入口文件。
  • router,整个应用的路由配置文件。
  • pages,整个应用的页面层级组件,一般router中的一个path,就对应这里的一个组件。
  • components,全局的基础组件。
  • service,api请求相关的服务封装。
  • redux,整个页面的数据管理存储。
  • utils,全局某些通用能力/配置,放置在里面。
  • assets,全局资源文件(图片资源、静态js库、svg、字体文件等等)

以上基本上是每一个前端工程的共识,但是除此之外,我们应该还有其他的共识。

1、页面pages,尽可能的组装业务,进行集中的资源调度。

也就是,我们尽可能的把业务相关的东西,都往pages层次的组件进行集成。

当我们从路由中,得到这路由对应的pages,往往预示着,它是一块相对独立的业务。比如:文件列表页、文件详情页等等。而对于人类而言,在一处地方看代码,比在多处看代码,更容易。

2、页面pages,尽可能的进行统一的数据管理。

在pages层面,进行统一的数据管理,数据管理往往是这么几种情况,从redux接入全局数据进行管理,向pages调度的子组件,通过props传递数据,或者针对Provider全局数据的注入的使用。(至于为什么,可以继续看下面数据管理部分)

3、页面pages,在拆分组件的时候,层级不应该过于多。

在pages的组件层面,很有可能,我们会遇到非常复杂的业务,导致我们的pages层面的组件,变得非常臃肿,这个时候,我们需要进行组件拆分,但是我建议,再只多拆分1层业务组件,不要拆分多个不同层级的组件。组件的层级过多,数据通信的复杂度就会提升,代码的可读性就会降低,如果这个时候,再配合不好的数据管理习惯,屎山代码,就已经形成了。(具体是为什么,可以继续看下面的组件拆分部分)

4、 每一个单元应具备原子性

pages层面,调度的每一个单元,应该具备一定的特性—原子性。

一个函数,只完成一件事情,比如事件响应,数据处理。

一个组件,它是纯粹的,它只和传递给它的props有关,和其他无关。

一个业务组件,也就是在pages过于复杂的情况下拆分出去的组件,虽然它具备业务属性,但是对于pages来说,它也是相对独立的。

3、组件的层级划分

个人比较推崇的组件层级划分方式:

  • pages层,pages层很简单,就是代表页面的意思,每一个路由path,它都对应一个page。
  • 业务组件层,一个page,很有可能很复杂,我们需要一定的设计,把这个页面分成几个块,然后由pages统一调度,实现我们的业务。
  • components层,也就是纯粹的UI组件,它只和props有关系,和其他无关。全局任意的地方,都可以调度。

为什么这么拆分呢?其实这是这么多年,针对真实业务场景,综合思考下来,得到的比较好的实践方式,形成这个组件划分的原则,是基于以下几个方面的思考。

1、辨识度高。

每一个路由,对应一个pages组件。

每一个components中的组件,都有与业务无关的纯UI组件。

根据业务的复杂度,中间产生了一层业务组件。

每一种组件,各司其职,边界清晰,方便我们看到一个组件,就知道这个组件是干嘛的的一种标识。

pages层的组件,进行统一的调度,数据管理,对接redux,组件拼接,事件交互等等。一个文件中的代码,阅读起来,也更加的容易。

业务组件,当pages层的组件,过于复杂的时候,我们把业务相对独立的单元,拆分出来,形成我们的业务组件。业务组件是可以对接redux,也可以有自己的数据状态,各种事件交互。(业务组件的核心,在于如何巧妙的进行边界设计,具体请参考下面的业务组件层的拆分思路)

components层的UI组件,纯粹的UI组件,与业务无关,在全局可复用。

2、结合业务拆分、代码可读性、复用性的一个综合结果。

多数情况下,我们可能没有一个清晰的思路,去做组件的划分工作。

遇到复杂的业务,我们本能的就进行组件的拆分,当时写的时候,没有考虑太多,但是写到一半,会发现,这个组件的变动,可能会引发其他组件的数据变动,我们就遇水搭桥,见招拆招,有些用redux解决,有些用props父子传递数据进行通信。

保持这样的习惯,我们可能拆分一个又一个组件,props传递了一层又一层,等过一段时间一看,自己都看不懂,自己写的代码是啥。

这是本能的,不想思考的,总想简单化的把项目完成。但是往往导致的结果是:本来简单的项目,代码写的越来越复杂!!

pages层面,负责数据的整体管理,组件调度,那么我们的通信就会比较方便,相当于pages层就是这个页面相关的业务的通信中心,我们基本上,能够通过props父子组件传递消息。

props父子组件,传递消息,注定了组件的层级不能过多,过多就会导致,数据就像套娃一样,一层又一层,导致可读性降低。

业务组件的出现,是为了解决,过于复杂的页面业务,我们可能需要进行拆分,把能够单独拆分出去的结构,独立出来,这样不仅从业务上进行了模块的拆分,也能提高不同模块的代码可读性。

那么,业务组件层的拆分思路是什么呢?

3、业务组件层的拆分思路

其实原则上,这里需要我们进行深入思考,哪些模块是独立的单元?页面中的哪些模块,和业务主体通信较少?

这其实就是核心,代码倒逼我们进行设计,进行深入思考,进行深入的业务理解。

思考点1:某一个模块,是不是相对独立的UI模块?

思考点2:某个模块是不是单独的业务单元?

思考点3:某个模块拆分出去,通信的代价到底大不大?

思考清楚这些,其实本质上,你思考的是,你的代码架构问题,你以什么样的视角,来解读你的UI、数据、业务的关联关系。

有了这些思考,你一定可以拆分相对合理的业务组件层。

4、数据管理的划分

多数前端,写代码的时候,并不会有数据中心,数据流向这些概念。

很多人还停留在,完成UI,渲染数据的层面。

如果是这样的思路,无论是组件划分,还是数据管理,注定做的一塌糊涂,代码成屎山是必然的。

组件划分的思路,其实也是一种数据管理的思路。

1、pages层,从天然的业务视角来看,他天然就是一块业务的集合,所以pages层作为一块单独的业务数据中心,它天然合适。

2、components层,我们定义了它,只和props相关,和其他无关,它被其他组件调度,天生注定了它通过props传递数据的行为模式。

3、业务层组件,我们定义了它是pages下面的一块单独业务,我们有必要对它进行合理的设计,它与pages组件的关系,是主模块与子模块的关系。可以通过props通信,也可以接入redux通信。

4、redux,大多数情况下,我们其实不需要用它,当数据的通信,不满足业务场景的时候,redux就是我们的解决方案。它真正的业务价值,在于跨pages的通信。比如,某个页面状态的更改,另一个页面状态,也跟着更改。

redux另一种常见用法:

redux的特性,在umi或者其他框架中,把redux作为数据管理中心来使用,所有的页面状态,业务相关的数据,都定义在redux中,所有的接口请求,用户行为,都是通过redux的dispath进行触发行为,来更改数据,数据通过props流入各个组件中。

这种方式,其实也是各种推崇的一种方式,但是这种方式对人的要求也高,表明了我们团队中的每一个人,都要熟悉并且接受这种数据管理的方式,才能写出相对一致,可读性高的代码。

只要其中的一部分人,不接受这样的数据管理方式,代码的管理它就会变得混乱,有些初始化数据,可能在pages组件中,有些可能发生在redux层,作为阅读者,很难排查代码执行的路径。

现实场景说明:

我们大多数情况下,对于redux的使用,并不清晰,redux的使用,是混乱的。

本不必要的数据,可能放在redux中管理。

本不必须接入redux的组件,非要接入redux。

redux中的dispath,调用的地方,千奇百怪。

数据的流向定义,完全杂乱无章。

这些东西,对于一个项目,都是灾难性的影响,它会导致,我们代码的迭代难度,急剧上升。

react高阶组件

一. 定义

  • 官方定义:参数为组件,返回值为新组件的函数

  • 本质:是函数而非组件,是对原有组件进行拦截封装的新组件,本质上是一种设计模式而非React API

 -   特点:
    -   接收一个组件作为参数
    -   返回一个新组件
    -   对新组件进行拦截和增强
  • 调用方式:const EnhancedComponent = higherOrderComponent(WrappedComponent)

  • 常见应用:

    • Redux中的connect函数(返回高阶组件)
    • React Router中的withRouter函数
  • 实现原理:

    • 结构:接收一个组件作为参数,返回一个新的增强组件
    • 命名规范:可通过displayName属性修改组件调试名称
    • 继承方式:新组件通常继承自PureComponent以获得性能优化

基础示例

import React, { PureComponent } from 'react'

// 定义一个高阶组件
function hoc(Cpn) {
  // 1.定义类组件
  class NewCpn extends PureComponent {
    render() {
      return <Cpn name="why"/>
    }
  }
  // 设置 displayName动态命名
  NewCpn.displayName = `HOC(${Cpn.displayName || Cpn.name || 'Component'})`;
  return NewCpn

  // 定义函数组件
  // function NewCpn2(props) {

  // }
  // return NewCpn2
}

class HelloWorld extends PureComponent {
  render() {
    return <h1>Hello World</h1>
  }
}

//直接命名
HelloWorld.displayName = 'HelloWorldComponent';

const HelloWorldHOC = hoc(HelloWorld)

export class App extends PureComponent {
  render() {
    return (
      <div>
        <HelloWorldHOC/>
      </div>
    )
  }
}

export default App

props增强

import { PureComponent } from 'react'

// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {
  class NewComponent extends PureComponent {
    constructor(props) {
      super(props)

      this.state = {
        userInfo: {
          name: "clare",
          level: 1
        }
      }
    }

    render() {
      return <OriginComponent {...this.props} {...this.state.userInfo}/>
    }
  }

  return NewComponent
}

export default enhancedUserInfo

import React, { PureComponent } from 'react'
import enhancedUserInfo from './hoc/enhanced_props'
import About from './pages/About'


const Home = enhancedUserInfo(function(props) {
  return <h1>Home: {props.name}-{props.level}-{props.banners}</h1>
})


export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home banners={["轮播1", "轮播2"]}/>   
      </div>
    )
  }
}

export default App

Context共享

import { createContext } from "react"

const ThemeContext = createContext()

export default ThemeContext



import ThemeContext from "../context/theme_context"

function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponment {...value} {...props}/>
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

export default withTheme



import React, { PureComponent } from 'react'
import withTheme from '../hoc/with_theme'
import ThemeContext from '../context/theme_context'



// export class Product extends PureComponent {
//   render() {
//     return (
//       <div>
//         Product:
//         <ThemeContext.Consumer>
//           {
//             value => {
//               return <h2>theme:{value.color}-{value.size}</h2>
//             }
//           }
//         </ThemeContext.Consumer>
//       </div>
//     )
//   }
// }

// export default Product

export class Product extends PureComponent {
  render() {
    const { color, size } = this.props

    return (
      <div>
        <h2>Product: {color}-{size}</h2>
      </div>
    )
  }
}

export default withTheme(Product)


import React, { PureComponent } from 'react'
import ThemeContext from './context/theme_context'
import Product from './pages/Product'

export class App extends PureComponent {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{color: "red", size: 30}}>
          <Product/>
        </ThemeContext.Provider>
      </div>
    )
  }
}

export default App

登录鉴权


function loginAuth(OriginComponent) {
  return props => {
    // 从localStorage中获取token
    const token = localStorage.getItem("token")

    if (token) {
      return <OriginComponent {...props}/>
    } else {
      return <h2>请先登录, 再进行跳转到对应的页面中</h2>
    }
  }
}

export default loginAuth

import React, { PureComponent } from 'react'
import loginAuth from '../hoc/login_auth'

export class Cart extends PureComponent {
  render() {
    return (
      <h2>Cart Page</h2>
    )
  }
}

export default loginAuth(Cart)


import React, { PureComponent } from 'react'
import Cart from './pages/Cart'

export class App extends PureComponent {
  constructor() {
    super()

    // this.state = {
    //   isLogin: false
    // }
  }

  loginClick() {
    localStorage.setItem("token", "hhh")

     this.setState({ isLogin: true })
    //this.forceUpdate()  //强制刷新用的较少
  }

  render() {
    return (
      <div>
        App
        <button onClick={e => this.loginClick()}>登录</button>
        <Cart/>
      </div>
    )
  }
}

export default App

二. 缺陷

  • 嵌套问题:需要包裹原组件,大量使用会产生深层嵌套
  • 调试困难:多层嵌套让props来源难以追踪
  • props劫持:可能意外覆盖传入的props(如name属性被覆盖)
  • 适用场景:类组件中仍常见,函数组件推荐使用Hooks

三. 其余高阶组件函数

memo组件作用

当父组件重新渲染时,React 默认会递归渲染所有子组件。memo 可以阻止子组件在 props 没有变化 时的重新渲染。

-   功能:类似PureComponent,对props进行浅比较(shallow compare),
-   原理:比较前后props差异决定是否重新渲染
-   本质:就是一个高阶组件,接收组件返回增强后的组件
import { useState, memo } from 'react';

// 使用 memo 包装子组件
const ChildComponent = memo(function ChildComponent({ name }) {
  console.log('ChildComponent 渲染了'); // 只有 name 变化时才会打印
  return <div>Hello, {name}!</div>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        计数: {count}
      </button>
      <button onClick={() => setName('Bob')}>
        改名
      </button>
      <ChildComponent name={name} />
    </div>
  );
}

注意事项


// 注意:如果传递对象、数组或函数,memo 可能失效
const ChildComponent = memo(function ChildComponent({ user, onClick }) {
  console.log('ChildComponent 渲染了');
  return <div onClick={onClick}>Hello, {user.name}!</div>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 每次都会创建新的对象和函数,导致 memo 失效
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>计数</button>
      <ChildComponent 
        user={{ name: 'Alice' }}  // 每次都创建新对象
        onClick={() => {}}         // 每次都创建新函数
      />
    </div>
  );
}

解决:使用 useMemo 和 useCallback 来保持引用稳定。

useMemo vs useCallback 对比

特性 useMemo useCallback
缓存函数 useMemo(() => fn, deps) useCallback(fn, deps)
缓存对象 useMemo(() => obj, deps) 不适用
返回值 缓存函数返回的值 直接缓存函数本身
等价关系 useCallback(fn, deps) = useMemo(() => fn, deps)

forwardRef作用

-   问题背景:函数组件无实例,无法直接绑定ref
-   解决方案:使用forwardRef将ref作为第二个参数传递
-   限制:仅适用于函数组件,类组件使用会报错

其余用法可查看: juejin.cn/post/718658…

import { useRef, forwardRef } from 'react';

// 简单的按钮组件
const FancyButton = forwardRef((props, ref) => {
return (
  <button ref={ref} style={{ padding: '10px 20px' }}>
    {props.children}
  </button>
);
});

function App() {
const buttonRef = useRef(null);

const focusButton = () => {
  buttonRef.current.focus(); // 聚焦按钮
};

return (
  <div>
    <FancyButton ref={buttonRef}>点击我</FancyButton>
    <button onClick={focusButton}>让上面按钮获得焦点</button>
  </div>
);
}

你可能忽略了useSyncExternalStore + useOptimistic + useTransition

在现代前端应用中,实时数据更新和顺滑交互体验已经成了标配:
聊天室、协作文档、实时监控面板……都离不开实时通信乐观更新

关于乐观更新的定义:

它是一种用户界面(UI)更新策略,
在执行异步操作(例如网络请求、数据库写入)之前,
先假设操作一定会成功
直接更新 UI 显示预期结果,
再在请求返回时根据实际结果确认或回滚

React 18 引入了几个强大的新 Hook:

  • useSyncExternalStore:让组件安全订阅外部状态;
  • useOptimistic:实现用户操作的“即时反馈”;
  • useTransition:让回滚或刷新变得平滑自然。

本文将通过一个实时聊天室的非完整案例,一步步带你理解这三者如何协同工作。

一、目标场景

我们要实现这样一个聊天室:

  1. 通过 WebSocket 接收服务器推送的消息;
  2. 用户输入后立即显示(不等服务端确认);
  3. 若发送失败,消息自动撤回或可重试。

看似简单,背后却包含了三种典型问题:

  • 外部状态同步(WebSocket);
  • 乐观 UI(即时显示用户操作);
  • 状态一致性与回滚(失败时撤回)。

二、第一步:用 useSyncExternalStore 管理实时状态

传统写法往往这样:

useEffect(() => {
  const ws = new WebSocket('ws://...');
  ws.onmessage = e => setMessages(JSON.parse(e.data));
}, []);

这没问题,但有几个隐患:

  • 多组件同时订阅时,可能重复连接;
  • React 并发模式下可能产生状态不同步;
  • 无法保证快照一致性。

React 官方提供的 useSyncExternalStore 专门解决这些问题。

实现 WebSocket Store

// websocketStore.js
let socket = null;
let messages = [];
const listeners = new Set();

function notify() {
  for (const listener of listeners) listener();
}

export function createWebSocketStore(url) {
  if (socket) return; // 避免重复连接
  socket = new WebSocket(url);

  socket.onmessage = (e) => {
    const data = JSON.parse(e.data);
    messages = [...messages, data];
    notify(); // 通知所有订阅组件更新
  };

  socket.onopen = () => console.log("✅ connected");
  socket.onclose = () => console.log("❌ closed");
}

export const store = {
  subscribe(listener) {
    listeners.add(listener);
    return () => listeners.delete(listener);
  },
  getSnapshot() {
    return messages;
  },
};

// 模拟发送函数
export function sendMessage(msg) {
  if (socket?.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify(msg));
  }
}

这段代码就是一个可订阅的全局状态容器
React 组件只需订阅它,就能安全地读取 WebSocket 状态。

三、第二步:用 useOptimistic 实现“消息先显示”

我们希望用户点击“发送”后,消息立刻显示在 UI 上,而不是等到服务器回应。

useOptimistic 就是为这种“乐观更新”场景设计的。

实现示例:

import { useEffect, useSyncExternalStore, useOptimistic } from "react";
import { store, createWebSocketStore, sendMessage } from "./websocketStore";

export default function ChatApp() {
  useEffect(() => {
    createWebSocketStore("wss://example.com/chat");
  }, []);

  const messages = useSyncExternalStore(store.subscribe, store.getSnapshot);

  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (currentMessages, newMsg) => [...currentMessages, { ...newMsg, optimistic: true }]
  );

  function handleSend(text) {
    const tempMsg = { user: "Asen", text, tempId: Date.now() };
    addOptimisticMessage(tempMsg);
    sendMessage(tempMsg);
  }

  return (
    <div className="p-4 max-w-md mx-auto">
      <h2 className="font-bold mb-2 text-lg">💬 实时聊天室</h2>

      <MessageList messages={optimisticMessages} />
      <ChatInput onSend={handleSend} />
    </div>
  );
}

function MessageList({ messages }) {
  return (
    <ul className="space-y-1">
      {messages.map((msg, i) => (
        <li
          key={msg.tempId || i}
          className={`p-2 rounded ${
            msg.optimistic ? "bg-gray-200 text-gray-500 italic" : "bg-blue-100"
          }`}
        >
          {msg.user}: {msg.text}
          {msg.optimistic && " (sending...)"} 
        </li>
      ))}
    </ul>
  );
}

现在,用户每次发消息都会立刻在屏幕上出现 (sending...)
真正的服务器消息到达后再被替换掉。

四、第三步:加入 useTransition 实现“失败回滚”

接下来,我们要让发送失败的消息自动撤回。
在实际项目中,这非常常见:网络波动、服务端延迟、权限问题等等。

修改 sendMessage

// websocketStore.js
export function sendMessage(msg) {
  return new Promise((resolve, reject) => {
    if (socket?.readyState !== WebSocket.OPEN) {
      reject("Socket not connected");
      return;
    }

    // 模拟 30% 发送失败
    setTimeout(() => {
      if (Math.random() < 0.3) {
        reject("Network error");
      } else {
        socket.send(JSON.stringify(msg));
        resolve();
      }
    }, 400);
  });
}

在组件中使用 useTransition 平滑回滚

import React, { useTransition } from "react";

const [isPending, startTransition] = useTransition();

async function handleSend(text) {
  const tempId = Date.now();
  const optimisticMsg = { user: "Asen", text, tempId, optimistic: true };
  addOptimisticMessage(optimisticMsg);

  try {
    await sendMessage({ user: "Asen", text });
  } catch (err) {
    console.error("❌ Failed:", err);
    startTransition(() => {
      addOptimisticMessage((msgs) =>
        msgs.filter((m) => m.tempId !== tempId)
      );
    });
  }
}

这样做的效果:

  • 消息先显示;
  • 如果失败,React 平滑地将其移除;
  • 整个过程无闪烁,无状态错乱。

五、三者协同背后的逻辑

Hook 作用 在本例中表现
useSyncExternalStore 安全订阅外部数据源 从 WebSocket 获取实时消息
useOptimistic 管理乐观状态(立即反馈) 发送时立即显示临时消息
useTransition 平滑更新、避免卡顿 回滚失败消息时保持流畅

三者组合形成一个非常稳定的结构:

用户操作 → useOptimistic (添加临时UI)
         ↓
真实异步操作 → 成功 → useSyncExternalStore 更新
                      ↳ 回滚 → useTransition 平滑撤销

六、应用场景

这种模式不仅能用于聊天室,还能应用在:

  • 实时评论区(评论先显示后确认)
  • 协作文档(本地编辑立即生效)
  • 实时监控(数据快速闪现)
  • 弹幕系统(先显示后同步)
  • 电商下单(先更新库存、后校验结果)

几乎所有实时 + 交互敏感的前端系统都能受益于这套组合。

React 双缓存架构与 diff 算法优化

提到 React 应用的页面更新优化策略,会有两个绕不开的概念,它们分别是双缓存架构和 diff 算法。 其中 React 利用双缓存架构在内存中生成下次要渲染的页面所对应的虚拟 DOM 树,并

别再被闭包坑了!React 19.2 官方新方案 useEffectEvent,不懂你就 OUT!

useEffectEvent:优雅解决 React 闭包陷阱的终极方案

在 React 开发中,闭包陷阱是开发者最常遇到的困扰之一。当组件状态更新时,我们希望某些逻辑能始终使用最新状态,却不想触发不必要的重渲染。React 19.2 引入的 useEffectEvent 正是为解决这一问题而生,它让代码更简洁、更安全,彻底告别闭包困扰。

闭包陷阱:问题根源

让我们从一个经典示例开始:

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme); // 闭包捕获了旧的 theme 值
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]); // theme 变化会导致不必要的重连
}

theme 状态变化时,useEffect 会重新执行,导致聊天室连接被重置。这并非我们想要的——我们只想更新通知主题,而非重连。

传统解决方案的痛点

过去,我们常使用 useRef 解决这个问题:

function ChatRoom({ roomId, theme }) {
  const themeRef = useRef(theme);
  themeRef.current = theme; // 手动更新 ref

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', themeRef.current); // 读取最新值
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
}

这种方式虽有效,但需要手动维护 ref,增加了代码复杂度和出错风险。

useEffectEvent:优雅的终极解决方案

React 19.2 引入的 useEffectEvent 让这一切变得简单:

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme); // ✅ 始终获取最新 theme
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', onConnected);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // 只依赖 roomId,无需 theme
}

为什么 useEffectEvent 是革命性的?

✨ 无需手动维护 ref

useEffectEvent 内部自动处理了最新值的捕获,无需再写 themeRef.current = theme

✨ 代码简洁度提升

依赖数组更短,逻辑更清晰,无需担心闭包陷阱,让代码更易读、易维护。

✨ 与 DOM 事件一致的行为

useEffectEvent 的行为类似于 DOM 事件,始终能获取最新的状态,无需额外处理。

实际应用场景:让代码更优雅

场景 1:自动滚动到底部

function ChatRoom() {
  const [messages, setMessages] = useState([]);
  const messagesEndRef = useRef(null);
  
  const scrollToBottom = useEffectEvent(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  });

  useEffect(() => {
    scrollToBottom();
  }, [messages]);
}

场景 2:WebSocket 消息处理

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  
  const handleMessage = useEffectEvent((message) => {
    setMessages(prev => [...prev, message]);
  });

  useEffect(() => {
    const socket = new WebSocket(`wss://example.com/${roomId}`);
    socket.onmessage = (event) => {
      handleMessage(JSON.parse(event.data));
    };
    return () => socket.close();
  }, [roomId]);
}

场景 3:表单自动保存

function Form() {
  const [input, setInput] = useState('');
  const [saved, setSaved] = useState(false);
  
  const saveForm = useEffectEvent(() => {
    if (input.length > 0) {
      setSaved(true); // 保存表单逻辑
    }
  });

  useEffect(() => {
    const timeout = setTimeout(() => {
      saveForm();
    }, 2000);
    return () => clearTimeout(timeout);
  }, [input]);
}

useEffectEvent 与 useRef 的全面对比

特性 useRef useEffectEvent
代码复杂度 高(需手动更新 ref) 低(自动处理)
依赖管理 需要额外管理 ref 更新 无需额外管理
闭包问题 需要额外处理 自动解决
适用场景 通用状态保存 专门用于副作用中的事件处理
代码可读性 降低 提升

使用注意事项

  1. 实验性功能useEffectEvent 仍处于实验阶段,目前仅在 React 19.2 的 Canary 版本中可用。
  2. 仅限副作用useEffectEvent 必须在 useEffect 内部使用。
  3. 不用于事件处理:不要将其直接作为 JSX 事件处理函数。
  4. 依赖数组useEffectEvent 本身不需要依赖数组,但其返回的函数必须在 useEffect 的依赖数组中声明。

结语

useEffectEvent 是 React 19.2 中真正解决闭包陷阱的革命性特性。它通过将事件逻辑与副作用解耦,让我们能写出更简洁、更安全的代码,避免不必要的重渲染,显著提升应用性能。

随着 React 的持续发展,这类工具将越来越完善,帮助我们更高效地构建 React 应用。现在就尝试在你的项目中使用 useEffectEvent,体验 React 开发的全新境界!

💡 现在就行动:确保你的 React 版本 >= 19.2,并安装 eslint-plugin-react-hooks@6.1.0 以获得最佳的 lint 支持。让闭包陷阱成为过去式,享受更优雅的 React 开发体验!

❌