普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月19日技术

【LeetCode 刷题系列 | 第 1 篇】前端老司机拆解接雨水问题,从暴力到最优解💦

2026年1月19日 09:56
🌧️ 前言 Hello~大家好。我是秋天的一阵风 欢迎来到我的 LeetCode 刷题系列专栏~ 作为一名深耕前端多年的老司机,我深知算法能力对前端工程师的重要性 —— 它不仅能帮我们在面试中脱颖而出

【LeetCode 刷题系列|第 2 篇】详解盛最多水的容器:从暴力到双指针的优化之路💧

2026年1月19日 09:55
🌊 前言 各位掘金的小伙伴们,咱们刷题系列又见面啦!今天要攻克的是 LeetCode 上的经典中等题 ——「盛最多水的容器」。这道题和上一篇讲的「接雨水」虽然都和 “水” 有关,但思路却大不相同:接雨

前端向架构突围系列 - 工程化(二):包管理工具的底层哲学与选型

2026年1月19日 09:39

写在前面

我们看技术本质、转变我们的思维、去理解去消化,不死记硬背。

如果说模块化规范(ESM/CJS)是前端工程的“交通法规”,那么包管理工具就是负责铺设道路的“基建大队”。而 node_modules,这个前端开发者最熟悉的黑洞(也是宇宙中最重的物体),往往也是工程治理中最大的痛点。

很多同学认为包管理仅仅是 npm installyarn add 的区别,但在架构师眼里,包管理的本质是 对依赖关系图谱(Dependency Graph)在磁盘物理空间上的投影与映射

既然要向架构突围,我们就不能只停留在命令行的使用上,必须把这个黑盒子拆开,看一看从嵌套地狱到硬链接黑科技的演进之路。

image.png


一、 混沌初开:嵌套结构的物理原罪

时光倒流回 npm v1/v2 的时代。那时候的设计哲学非常简单粗暴: “依赖树长什么样,磁盘文件结构就长什么样。”

假设你的项目依赖了 AA 又依赖了 B (v1.0),同时你的项目还直接依赖了 CC 也依赖了 B (v2.0)。在 npm v2 中,磁盘结构是严格递归的:

Plaintext

node_modules
├── A
│   └── node_modules
│       └── B (v1.0)
└── C
    └── node_modules
        └── B (v2.0)

这种“诚实”的设计虽然保证了绝对的隔离,但也带来了两个严重的工程灾难:

  1. 冗余(Redundancy): 如果 AC 依赖的是 同一个版本BB 也会被重复安装两次。对于大型项目,几百个重复的包会瞬间吃光磁盘空间。
  2. 路径地狱(Path Hell): Windows 系统曾经有 260 个字符的路径长度限制。当依赖层级过深时(A/node_modules/B/node_modules/C...),文件甚至无法被操作系统删除,导致了无数开发者的崩溃。

二、 扁平化的代价:幽灵与分身

为了解决嵌套地狱,npm v3 和 Yarn v1 引入了 “扁平化(Hoisting)” 机制。这是前端工程史上的一次重要妥协。

它们尝试把所有依赖都提升到项目根目录的 node_modules 下。于是,结构变成了这样:

Plaintext

node_modules
├── A
├── B (v1.0)  <-- 被提升了,大家都共用这一份
└── C
    └── node_modules
        └── B (v2.0) <-- 版本冲突,只能委屈留在下面

这次变革解决了路径过深的问题,并复用了依赖,但它打开了潘多拉的魔盒,释放了两只“怪兽”:

1. 幽灵依赖(Phantom Dependencies)

在上面的例子中,你的 package.json 里并没有声明 B。但是因为 B 被提升到了顶层,你的代码里竟然可以直接 import B 并且能跑通! 这非常危险。如果有一天 A 升级了,不再依赖 B,或者 AB 的版本换了,你的项目就会莫名其妙地崩溃。这就是“明明没装这个包,为什么能用”的灵异现象。

2. 分身依赖(Doppelgangers)

如果你的项目里有 100 个包依赖 lodash@4.0.0,还有 1 个包依赖 lodash@3.0.0。 如果运气不好,3.0.0 被提升到了顶层,那么那 100 个包就没法复用顶层,只能各自在自己的 node_modules 下再装一份 4.0.0。 结果就是你拥有了 101 份 lodash。这不仅浪费空间,还会导致 单例模式失效(比如 React 或 Styled-components 多实例共存引发的 Hooks call 报错)。


三、 破局者:pnpm 的链接魔法

当我们意识到“扁平化”并非银弹时,社区开始寻找新的出路。这时候,pnpm 带着它的 硬链接(Hard Link)符号链接(Symbolic Link) 登场了。

pnpm 的设计哲学彻底颠覆了之前的认知:它不再试图把依赖拷贝到项目里,而是把依赖“挂载”到项目里。

1. 内容寻址存储(CAS)

pnpm 在全局维护了一个 .pnpm-store。所有包都只存在于这里。同一个包的同一个版本,在你的硬盘上 永远只有一份

2. 非扁平化的 node_modules

如果你用 pnpm 安装,你会发现项目根目录的 node_modules 里只有你 显式声明 的包(这就直接杀死了幽灵依赖)。 但这些包其实只是软链接(Symlink),它们指向 node_modules/.pnpm 下的虚拟仓库,而虚拟仓库里的文件又是通过硬链接指向全局 Store 的。

这种架构同时实现了:

  • 严格性: 只有 package.json 里写的才能 require。
  • 磁盘效率: 跨项目复用,极速安装。

四、 激进派:Yarn Berry (PnP) 的无盘化理想

Yarn v2+ (Berry) 走得更远,它提出了 PnP (Plug'n'Play) 模式,试图彻底消灭 node_modules

它的思路是:既然 Node.js 无论如何都要去查文件,为什么不直接生成一个映射表(.pnp.cjs),告诉 Node "你要找的 React 在磁盘的这个位置",而不需要把文件真的拷贝过去?

这是最理想的形态,但因为它破坏了 Node.js 原生的模块解析规则(Node 默认就是去目录里找文件的),导致对现有生态的兼容性成本极高。这也是为什么 PnP 至今叫好不叫座的原因。


五、 架构师的治理策略:选型与规范

在 2025 年这个时间节点,作为架构师,该如何为团队制定包管理策略?

1. 选型建议

  • 默认首选 pnpm: 它是目前的“版本答案”。它在严格性(避免幽灵依赖)和性能(磁盘空间与安装速度)之间取得了完美的平衡。
  • Monorepo 必备: pnpm 的 Workspace 支持几乎是目前多包架构的标准配置。通过 workspace: 协议,你可以轻松实现本地包之间的相互引用,而无需发版。
  • 慎用 Bun: 虽然 Bun 作为一个 Runtime 自带极速包管理,但在企业级大仓中,其边缘 Case 的处理和对 postinstall 脚本的兼容性仍需时间检验。

2. 锁文件(Lockfile)治理

不要小看 pnpm-lock.yamlyarn.lock。它是团队协作的唯一真理。

  • CI/CD 里的严谨性: 在构建脚本中,永远使用 npm ci / pnpm install --frozen-lockfile。这能确保如果 Lock 文件和 package.json 不匹配,构建直接失败,而不是悄悄更新版本导致线上 Bug。
  • Conflict 处理: 遇到 Lock 文件冲突,严禁直接删掉 Lock 文件重新生成!这会导致所有依赖版本即使在语义化版本(SemVer)范围内也会发生漂移。正确的做法是手动解决冲突,或者单独升级冲突的那个包。

3. 依赖清洗

定期检查项目中的 dependenciesdevDependencies 归属是否正确。构建工具(Webpack/Vite)插件应该放在 dev 里,而 React/Vue/Lodash 等运行时依赖必须放在 dependencies 里。在 Docker 构建等场景下,我们会使用 npm install --production 来剔除开发依赖,如果放错位置,线上服务就会起不来。


Next Step: 搞定了依赖治理,我们的代码终于可以安全地跑在开发环境了。但如何把成千上万个文件变成浏览器能看懂的产物?下一节,我们将深入构建工具的腹地—— 《第三篇:引擎(上)——Webpack 的兴衰与构建工具的本质》

flutter-实现瀑布流布局及下拉刷新上拉加载更多

作者 鹏多多
2026年1月19日 08:47

在 Flutter 应用开发中,瀑布流布局常用于展示图片、商品列表等需要以不规则但整齐排列的内容。同时,下拉刷新和上拉加载更多功能,能够极大提升用户体验,让用户方便地获取最新和更多的数据。

1. 前置条件

  1. Flutter 环境已搭建(要求 Flutter 3.0+、Dart 2.17+)

  2. 本地图片资源已放入 assets/images 目录,并在 pubspec.yaml 中配置

  3. 已安装依赖插件并执行 flutter pub get

2. 结构分析

6210b7bafa9244369d7ce1aa5ca874be.gif

2.1 安装依赖插件

在项目的 pubspec.yaml 文件中添加以下依赖:


# 瀑布流布局:https://pub.dev/packages/waterfall_flow
waterfall_flow: ^3.0.3
# 上拉加载更多+下拉刷新:https://pub.dev/packages/pull_to_refresh
pull_to_refresh: ^2.0.0

注意:waterfall_flow: ^3.0.3 需适配 Flutter 3.0+,pull_to_refresh: ^2.0.0 需 Dart 2.17+

添加依赖后执行命令安装:


flutter pub get

2.2 配置本地图片资源

pubspec.yaml 中配置图片资源路径,确保 Flutter 能识别本地图片:


flutter:
  uses-material-design: true
  # 配置图片资源目录
  assets:
    - assets/images/

2.3 引入必要的库


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:waterfall_flow/waterfall_flow.dart';
  • dart:async:提供异步操作能力,用于处理刷新和加载更多的延迟模拟。

  • package:flutter/material.dart:Flutter 核心 UI 库,提供 Scaffold、Container 等基础组件。

  • package:pull_to_refresh/pull_to_refresh.dart:实现下拉刷新和上拉加载更多的核心库。

  • package:waterfall_flow/waterfall_flow.dart:用于快速构建瀑布流布局的第三方库。

2.4 定义图片枚举及扩展

为了规范图片资源管理,我们定义图片枚举,并通过扩展方法获取图片路径:


/// 本地图片枚举
enum ImageEnum {
  banner1,
  banner2,
  banner3,
  model1,
  model2,
  model3,
  model4,
}

/// 为枚举扩展获取图片路径的方法
extension ImageEnumExtension on ImageEnum {
  String get path {
    switch (this) {
      case ImageEnum.banner1:
        return 'assets/images/banner1.png';
      case ImageEnum.banner2:
        return 'assets/images/banner2.png';
      case ImageEnum.banner3:
        return 'assets/images/banner3.png';
      case ImageEnum.model1:
        return 'assets/images/model1.png';
      case ImageEnum.model2:
        return 'assets/images/model2.png';
      case ImageEnum.model3:
        return 'assets/images/model3.png';
      case ImageEnum.model4:
        return 'assets/images/model4.png';
    }
  }
}

2.5 定义 ImageWaterfallFlow 组件

定义有状态组件,用于管理瀑布流布局的状态和 UI 渲染:


class ImageWaterfallFlow extends StatefulWidget {
  const ImageWaterfallFlow({super.key});

  @override
  State<ImageWaterfallFlow> createState() => ImageWaterfallFlowState();
}

有状态组件可以根据用户操作或数据变化动态更新 UI,createState 方法返回状态管理类 ImageWaterfallFlowState

2.6 ImageWaterfallFlowState 类的详细解析

该类是组件的核心,负责管理数据、控制器和业务逻辑:


class ImageWaterfallFlowState extends State<ImageWaterfallFlow> {
  /// 字体样式
  final TextStyle myTxtStyle = const TextStyle(
      color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800);

  /// 模拟数据(初始数据)- 增加泛型提升类型安全
  List<ImageEnum> imageList = [
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4,
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4
  ];

  /// 模拟数据(加载更多使用)
  List<ImageEnum> moreList = [ImageEnum.banner1, ImageEnum.banner2, ImageEnum.banner3];

  /// 上拉下拉控制器
  final RefreshController myRefreshController = RefreshController();
  • myTxtStyle:定义图片上显示序号的字体样式。

  • imageList:存储瀑布流初始展示的图片数据,使用泛型 List<ImageEnum> 保证类型安全。

  • moreList:存储加载更多时需要追加的数据。

  • myRefreshController:来自 pull_to_refresh 库,用于控制刷新和加载的状态。

2.7 刷新和加载更多的方法

实现下拉刷新和上拉加载更多的业务逻辑,补充边界条件处理:


/// 刷新
void onRefresh() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  // 模拟刷新:恢复初始数据
  setState(() {
    imageList = [
      ImageEnum.banner1,
      ImageEnum.banner2,
      ImageEnum.banner3,
      ImageEnum.model1,
      ImageEnum.model2,
      ImageEnum.model3,
      ImageEnum.model4,
      ImageEnum.banner1,
      ImageEnum.banner2,
      ImageEnum.banner3,
      ImageEnum.model1,
      ImageEnum.model2,
      ImageEnum.model3,
      ImageEnum.model4
    ];
  });
  myRefreshController.refreshCompleted();
  myRefreshController.resetNoData(); // 重置无更多数据状态
}

/// 加载更多
void onLoadMore() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  // 模拟:数据超过30条时标记无更多数据
  if (imageList.length >= 30) {
    myRefreshController.loadNoData();
  } else {
    imageList.addAll(moreList);
    if (mounted) {
      setState(() {});
    }
    myRefreshController.loadComplete();
  }
}
  • onRefresh:下拉刷新时触发,模拟 1 秒延迟后恢复初始数据,并重置加载状态。

  • onLoadMore:上拉加载时触发,模拟 1 秒延迟后追加数据;当数据量超过 30 条时,标记为“无更多数据”。

  • mounted 判断:防止组件销毁后调用 setState 导致异常。

2.8 资源释放

重写 dispose 方法,释放控制器资源,避免内存泄漏:


@override
void dispose() {
  myRefreshController.dispose(); // 释放刷新控制器
  super.dispose();
}

2.9 构建 UI 的方法

构建组件的基础布局结构:


/// 布局
@override
Widget build(BuildContext context) {
  return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
          child: SizedBox(
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              child: listWidget())));
}
  • Scaffold:作为页面的根布局,设置背景色为黑色。

  • SafeArea:避免内容被刘海屏、状态栏等遮挡。

  • SizedBox:设置与屏幕同宽同高的容器,承载瀑布流列表。

2.10 构建瀑布流列表的方法

整合刷新组件和瀑布流布局,实现核心 UI 效果:


/// 列表
Widget listWidget() {
  return SmartRefresher(
    enablePullDown: true,
    enablePullUp: true,
    header: const ClassicHeader(),
    footer: const ClassicFooter(),
    controller: myRefreshController,
    onRefresh: onRefresh,
    onLoading: onLoadMore,
    child: WaterfallFlow.builder(
      padding: const EdgeInsets.all(10), // 增加内边距,避免内容贴边
      physics: const BouncingScrollPhysics(),
      gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 20,
        mainAxisSpacing: 20,
        viewportBuilder: (int index1, int index2) {
          print('变化:$index1-$index2');
        },
        // 修正最后一个子项的判断逻辑(索引从0开始)
        lastChildLayoutTypeBuilder: (index) => index == imageList.length - 1
            ? LastChildLayoutType.fullCrossAxisExtent
            : LastChildLayoutType.none,
      ),
      itemCount: imageList.length,
      itemBuilder: (BuildContext context, int index) {
        return Container(
          color: Colors.white,
          height: (index + 1) % 2 == 0 ? 100 : 200,
          child: Container(
              alignment: Alignment.center,
              decoration: BoxDecoration(
                  color: Colors.blue.shade300,
                  image: DecorationImage(
                    image: AssetImage(imageList[index].path), // 调用扩展方法获取图片路径
                    fit: BoxFit.cover,
                  )),
              child: Text('第${index + 1}张', style: myTxtStyle)),
        );
      },
    ),
  );
}
  • SmartRefresherpull_to_refresh 库的核心组件,配置上下拉开关、头部底部样式和回调方法。

  • WaterfallFlow.builder:瀑布流布局的构建器,通过懒加载方式渲染子项:

    • padding:增加内边距,优化视觉效果。

    • crossAxisCount: 2:设置瀑布流为 2 列布局。

    • crossAxisSpacing/mainAxisSpacing:设置子项之间的水平和垂直间距。

    • lastChildLayoutTypeBuilder:修正索引判断逻辑,让最后一个子项占满整行宽度。

    • itemBuilder:构建每个瀑布流子项,通过 AssetImage(imageList[index].path) 获取图片路径,设置不同高度实现瀑布流效果。

3. 核心库 API & 属性说明

3.1 waterfall_flow 核心 API/属性

3.1.1 WaterfallFlow 核心组件

属性名 类型 说明 默认值
padding EdgeInsetsGeometry 瀑布流整体内边距 EdgeInsets.zero
physics ScrollPhysics 滚动物理效果 AlwaysScrollableScrollPhysics()
shrinkWrap bool 是否根据子项高度自适应 false
gridDelegate SliverWaterfallFlowDelegate 瀑布流布局代理(核心) 无(必传)
children List<Widget> 子组件列表(非懒加载) []
itemCount int 子项数量(builder 模式) 无(builder 模式必传)
itemBuilder IndexedWidgetBuilder 子项构建器(懒加载) 无(builder 模式必传)

3.1.2 SliverWaterfallFlowDelegateWithFixedCrossAxisCount(常用布局代理)

属性名 类型 说明 默认值
crossAxisCount int 列数(核心) 无(必传)
crossAxisSpacing double 列之间的间距 0.0
mainAxisSpacing double 行之间的间距 0.0
lastChildLayoutTypeBuilder LastChildLayoutTypeBuilder 最后一个子项布局类型 null
viewportBuilder ViewportBuilder 视口内子项变化回调 null
collectGarbage CollectGarbage 回收不可见子项时的回调 null

3.1.3 LastChildLayoutType(最后一个子项布局类型)

枚举值 说明
LastChildLayoutType.none 无特殊布局(默认)
LastChildLayoutType.fullCrossAxisExtent 占满整行宽度
LastChildLayoutType.footnote 脚注样式(小尺寸)

3.2 pull_to_refresh 核心 API/属性

3.2.1 SmartRefresher(核心组件)

属性名 类型 说明 默认值
enablePullDown bool 是否启用下拉刷新 true
enablePullUp bool 是否启用上拉加载 false
header RefreshHeader 下拉刷新头部样式 ClassicHeader()
footer LoadFooter 上拉加载底部样式 ClassicFooter()
controller RefreshController 刷新/加载控制器(核心) 无(必传)
onRefresh VoidCallback? 下拉刷新回调 null
onLoading VoidCallback? 上拉加载回调 null
child Widget 包裹的子组件(如列表/瀑布流) 无(必传)
scrollDirection Axis 滚动方向 Axis.vertical
physics ScrollPhysics 滚动物理效果 ClampingScrollPhysics()
enableTwoLevel bool 是否启用二级刷新(如下拉展开更多) false

3.2.2 RefreshController(核心控制器)

方法名 说明
refreshCompleted() 标记下拉刷新完成
refreshFailed() 标记下拉刷新失败
loadComplete() 标记上拉加载完成
loadNoData() 标记无更多数据
resetNoData() 重置无更多数据状态
dispose() 释放控制器资源(必写,避免内存泄漏)
requestRefresh() 主动触发下拉刷新
requestLoading() 主动触发上拉加载

3.2.3 ClassicHeader/ClassicFooter(经典样式)

属性名 类型 说明 默认值
height double 头部/底部高度 60.0
idleText String 闲置状态文本 下拉刷新/上拉加载
refreshingText String 刷新中/加载中文本 刷新中/加载中
completeText String 完成状态文本 刷新完成/加载完成
failedText String 失败状态文本 刷新失败/加载失败
noDataText String 无更多数据文本 暂无更多数据
textStyle TextStyle 文本样式 灰色 14 号字
iconSize double 图标大小 20.0

4. 完整代码


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:waterfall_flow/waterfall_flow.dart';

/// 本地图片枚举
enum ImageEnum {
  banner1,
  banner2,
  banner3,
  model1,
  model2,
  model3,
  model4,
}

/// 为枚举扩展获取图片路径的方法
extension ImageEnumExtension on ImageEnum {
  String get path {
    switch (this) {
      case ImageEnum.banner1:
        return 'assets/images/banner1.png';
      case ImageEnum.banner2:
        return 'assets/images/banner2.png';
      case ImageEnum.banner3:
        return 'assets/images/banner3.png';
      case ImageEnum.model1:
        return 'assets/images/model1.png';
      case ImageEnum.model2:
        return 'assets/images/model2.png';
      case ImageEnum.model3:
        return 'assets/images/model3.png';
      case ImageEnum.model4:
        return 'assets/images/model4.png';
    }
  }
}

/// 瀑布流组件
class ImageWaterfallFlow extends StatefulWidget {
  const ImageWaterfallFlow({super.key});

  @override
  State<ImageWaterfallFlow> createState() => ImageWaterfallFlowState();
}

class ImageWaterfallFlowState extends State<ImageWaterfallFlow> {
  /// 字体样式
  final TextStyle myTxtStyle = const TextStyle(
      color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800);

  /// 模拟数据(初始数据)- 增加泛型提升类型安全
  List<ImageEnum> imageList = [
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4,
    ImageEnum.banner1,
    ImageEnum.banner2,
    ImageEnum.banner3,
    ImageEnum.model1,
    ImageEnum.model2,
    ImageEnum.model3,
    ImageEnum.model4
  ];

  /// 模拟数据(加载更多使用)
  List<ImageEnum> moreList = [ImageEnum.banner1, ImageEnum.banner2, ImageEnum.banner3];

  /// 上拉下拉控制器
  final RefreshController myRefreshController = RefreshController();

  /// 刷新
  void onRefresh() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    // 模拟刷新:恢复初始数据
    setState(() {
      imageList = [
        ImageEnum.banner1,
        ImageEnum.banner2,
        ImageEnum.banner3,
        ImageEnum.model1,
        ImageEnum.model2,
        ImageEnum.model3,
        ImageEnum.model4,
        ImageEnum.banner1,
        ImageEnum.banner2,
        ImageEnum.banner3,
        ImageEnum.model1,
        ImageEnum.model2,
        ImageEnum.model3,
        ImageEnum.model4
      ];
    });
    myRefreshController.refreshCompleted();
    myRefreshController.resetNoData(); // 重置无更多数据状态
  }

  /// 加载更多
  void onLoadMore() async {
    await Future.delayed(const Duration(milliseconds: 1000));
    // 模拟:数据超过30条时标记无更多数据
    if (imageList.length >= 30) {
      myRefreshController.loadNoData();
    } else {
      imageList.addAll(moreList);
      if (mounted) {
        setState(() {});
      }
      myRefreshController.loadComplete();
    }
  }

  @override
  void dispose() {
    myRefreshController.dispose(); // 释放控制器资源
    super.dispose();
  }

  /// 布局
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.black,
        body: SafeArea(
            child: SizedBox(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                child: listWidget())));
  }

  /// 列表
  Widget listWidget() {
    return SmartRefresher(
      enablePullDown: true,
      enablePullUp: true,
      header: const ClassicHeader(),
      footer: const ClassicFooter(),
      controller: myRefreshController,
      onRefresh: onRefresh,
      onLoading: onLoadMore,
      child: WaterfallFlow.builder(
        padding: const EdgeInsets.all(10),
        physics: const BouncingScrollPhysics(),
        gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 20,
          mainAxisSpacing: 20,
          viewportBuilder: (int index1, int index2) {
            print('变化:$index1-$index2');
          },
          // 修正最后一个子项的判断逻辑
          lastChildLayoutTypeBuilder: (index) => index == imageList.length - 1
              ? LastChildLayoutType.fullCrossAxisExtent
              : LastChildLayoutType.none,
        ),
        itemCount: imageList.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            color: Colors.white,
            height: (index + 1) % 2 == 0 ? 100 : 200,
            child: Container(
                alignment: Alignment.center,
                decoration: BoxDecoration(
                    color: Colors.blue.shade300,
                    image: DecorationImage(
                      image: AssetImage(imageList[index].path),
                      fit: BoxFit.cover,
                    )),
                child: Text('第${index + 1}张', style: myTxtStyle)),
          );
        },
      ),
    );
  }
}

5. 常见问题 & 解决方案

  1. 图片不显示

    • 检查 pubspec.yamlassets 配置路径是否与实际图片存放路径一致。

    • 执行 flutter clean 清理缓存后,重新运行项目。

    • 确认枚举扩展方法 path 返回的路径正确。

  2. 加载更多无响应

    • 确认 SmartRefresherenablePullUp 属性设置为 true

    • 检查 RefreshController 是否正确关联到 SmartRefresher

    • 确保 onLoading 回调方法已正确绑定。

  3. 瀑布流布局错乱

    • 避免子项高度差异过大,可根据实际需求调整高度规则。

    • 检查 crossAxisCountcrossAxisSpacing 等参数配置是否冲突。

    • 确认 lastChildLayoutTypeBuilder 的索引判断逻辑正确。

  4. 内存泄漏

    • 务必在 dispose 方法中调用 myRefreshController.dispose() 释放控制器。

    • 避免在异步操作中未判断 mounted 就调用 setState

6. 总结

通过 waterfall_flowpull_to_refresh 两个第三方库,我们快速实现了 Flutter 瀑布流布局,并集成了下拉刷新和上拉加载更多功能。

该方案可直接应用于图片列表、商品展示等常见业务场景,也可根据实际需求扩展子项点击、图片懒加载、自定义刷新样式等功能。

希望这篇文章能帮助你理解并在自己的 Flutter 项目中运用类似的功能。


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

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

往期文章

栗子前端技术周刊第 113 期 - Angular 21.1、pnpm 10.28、Bun v1.3.6...

2026年1月19日 08:46

🌰栗子前端技术周刊第 113 期 (2026.01.12 - 2026.01.18):浏览前端一周最新消息,学习国内外优秀文章,让我们保持对前端的好奇心。

📰 技术资讯

  1. Angular 21.1:Angular 21.1 已发布,更新内容包括:为 Cloudflare 和 Cloudinary 图像加载器添加自定义转换、支持 ImageKit 和 Imgix 加载器中的自定义转换、添加路由清理控制等等。

  2. pnpm 10.28:这款高效的包管理器新增了 beforePacking 钩子,可在发布时自定义 package.json 的内容。

  3. Bun v1.3.6Bun.Archive 现在可以处理 tar 归档文件,Bun.JSONC 支持解析带注释的 JSON,此外还包含许多性能优化和调整。

  4. jQuery 二十周年:本周是 jQuery 发布的二十周年。

📒 技术文章

  1. How Browsers Work:浏览器是如何工作的 - 一份关于浏览器工作原理的交互式指南。

  2. Date is Out, Temporal is In:Date 已过时,Temporal 正当时 - 多年来,Temporal API 一直被视为解决 JavaScript Date 对象缺陷的未来方案,但这一 “未来” 终于到来了。Mat 通过大量示例展示了 Date 的不足之处,并以此凸显了 Temporal 的优势。

  3. 视频播放弱网提示实现:文章围绕视频播放弱网提示实现展开,文中提到了 NetworkInformation 和监听 Video 元素事件两种方法。

🔧 开发工具

  1. memlab 2.0:一个用于发现 JavaScript 内存泄漏的框架。这是一个用于识别内存泄漏和优化方法的测试与分析框架,源自 Meta 优化其主应用的内部方案。编写测试场景后,memlab 会对比堆快照、过滤内存泄漏并汇总结果。
image-20260118085436938
  1. Superdiff 3.2:比较两个数组或对象并返回差异,Superdiff 的近期更新提升了性能,增加了对流式输入的支持,并可以使用 Web Worker 在独立线程中进行更高效的差异比对。
image-20260118085226505
  1. Fabric.js 7:一款基于 JavaScript 的 HTML5 Canvas 库。它在 canvas 元素之上提供了一套对象模型,同时支持 SVG 转 canvas、canvas 转 SVG 的双向格式转换功能。此外,该库还配备了大量附带完整代码的示例供开发者参考使用。
image-20260118085320730

🚀🚀🚀 以上资讯文章选自常见周刊,如 JavaScript Weekly 等,周刊内容也会不断优化改进,希望你们能够喜欢。

💖 欢迎关注微信公众号:栗子前端

React从入门到出门第十章 Fiber 架构升级与调度系统优化

作者 怕浪猫
2026年1月19日 08:43

image.jpg 大家好~ 相信很多 React 开发者都有过这样的困惑:为什么我的组件明明只改了一个状态,却感觉页面卡顿?为什么有时候异步更新的顺序和预期不一样?其实这些问题的根源,都和 React 的底层架构——Fiber,以及它的调度系统密切相关。

从 React 16 引入 Fiber 架构至今,它经历了多次迭代优化,而 React 19 更是在 Fiber 架构和调度系统上做了不少关键升级,进一步提升了应用的流畅度和性能。今天这篇文章,我们就用“浅显易懂的语言+丰富的案例+直观的图例”,把 React 19 的 Fiber 架构和调度系统讲透:从 Fiber 解决的核心问题,到它的升级点,再到调度系统如何智能分配任务,让你不仅知其然,更知其所以然~

一、先搞懂:为什么需要 Fiber 架构?

在聊 React 19 的升级之前,我们得先明白:Fiber 架构是为了解决什么问题而诞生的?这就要从 React 15 及之前的 Stack Reconciliation(栈协调)说起。

1. 旧架构的痛点:不可中断的“长任务”

React 15 的栈协调机制,本质上是一个“递归递归过程”。当组件树需要更新时(比如状态变化、props 改变),React 会从根组件开始,递归遍历整个组件树,进行“虚拟 DOM 对比”(Reconciliation)和“DOM 操作”。这个过程有个致命问题:一旦开始,就无法中断

浏览器的主线程是“单线程”,既要处理 JS 执行,也要处理 UI 渲染(重排、重绘)、用户交互(点击、输入)等任务。如果递归遍历的组件树很深、任务很重,这个“长任务”会占据主线程很长时间(比如几百毫秒),导致浏览器无法及时响应用户操作,出现页面卡顿、输入延迟等问题。

2. 案例:栈协调的卡顿问题

假设我们有一个嵌套很深的组件树:

// 嵌套很深的组件树
function App() {
  const [count, setCount] = useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>
      <Level1 />
      <Level2 />
      {/* ... 嵌套 100 层 Level 组件 ... */}
      <Level100 />
      <p>计数:{count}</p>
    </div>
  );
}

当我们点击页面修改 count 时,React 15 会递归遍历这 100 层组件,进行虚拟 DOM 对比。这个过程会占据主线程几十甚至几百毫秒,期间用户如果再点击、输入,浏览器完全无法响应,出现明显卡顿。

3. Fiber 的核心目标:让任务“可中断、可恢复”

为了解决这个问题,React 16 引入了 Fiber 架构,核心目标是:将不可中断的递归遍历,拆分成可中断、可恢复的小任务。通过这种方式,React 可以在执行这些小任务的间隙,“还给”浏览器主线程时间,让浏览器有机会处理用户交互、UI 渲染等紧急任务,从而避免卡顿。

这就像我们平时工作:旧架构是“一口气做完一整套复杂工作,中间不休息”,容易累倒且无法响应突发情况;Fiber 架构是“把复杂工作拆成一个个小任务,做一个小任务就看看有没有紧急事,有就先处理紧急事,处理完再继续做剩下的小任务”,效率更高、更灵活。

二、React 19 Fiber 架构:核心升级点拆解

Fiber 架构的核心是“Fiber 节点”和“双缓存机制”。React 19 在继承这一核心的基础上,做了三个关键升级:优化 Fiber 节点结构增强任务优先级区分优化双缓存切换效率。我们先从最基础的 Fiber 节点开始讲起。

1. 基础:Fiber 节点是什么?

在 Fiber 架构中,每个组件都会对应一个“Fiber 节点”。Fiber 节点不仅存储了组件的类型、属性(props)、状态(state)等信息,更重要的是,它还存储了“任务调度相关的信息”,比如:

  • 当前任务的优先级;
  • 下一个要处理的 Fiber 节点(用于链表遍历,替代递归);
  • 任务是否已完成、是否需要中断;
  • 对应的 DOM 元素。

可以把 Fiber 节点理解为“组件的任务说明书”,它让 React 不仅知道“这个组件是什么”,还知道“该怎么处理这个组件的更新任务”。

2. React 19 Fiber 节点结构优化(简化代码模拟)

我们用简化的 JS 代码,模拟 React 19 中 Fiber 节点的核心结构(真实结构更复杂,这里只保留关键字段):

// React 19 Fiber 节点结构(简化版)
class FiberNode {
  constructor(type, props) {
    this.type = type; // 组件类型(如 'div'、FunctionComponent)
    this.props = props; // 组件属性
    this.state = null; // 组件状态
    this.dom = null; // 对应的真实 DOM 元素

    // 调度相关字段(React 19 优化点)
    this.priority = 0; // 任务优先级(1-5,数字越大优先级越高)
    this.deferredExpirationTime = null; // 延迟过期时间(用于低优先级任务)

    // 链表结构相关字段(替代递归,实现可中断遍历)
    this.child = null; // 第一个子 Fiber 节点
    this.sibling = null; // 下一个兄弟 Fiber 节点
    this.return = null; // 父 Fiber 节点

    // 双缓存相关字段
    this.alternate = null; // 对应的另一个 Fiber 树节点
    this.effectTag = null; // 需要执行的 DOM 操作(如插入、更新、删除)
  }
}

React 19 的核心优化点之一,就是精简了 Fiber 节点的冗余字段,同时增强了优先级相关字段的精度。比如新增的 deferredExpirationTime 字段,可以更精准地控制低优先级任务的执行时机,避免低优先级任务“饿死”(一直得不到执行)。

3. 核心:链表遍历替代递归(可中断的关键)

React 15 用递归遍历组件树,而 React 19 基于 Fiber 节点的链表结构,用“循环遍历”替代了递归。这种遍历方式的核心是“从根节点开始,依次处理每个 Fiber 节点,处理完一个节点后,记录下一个要处理的节点,随时可以中断”。

我们用简化代码模拟这个遍历过程:

// 模拟 React 19 Fiber 树遍历(循环遍历,可中断)
function traverseFiberTree(rootFiber) {
  let currentFiber = rootFiber;

  // 循环遍历,替代递归
  while (currentFiber !== null) {
    // 1. 处理当前 Fiber 节点(比如虚拟 DOM 对比、计算需要的 DOM 操作)
    processFiber(currentFiber);

    // 2. 检查是否需要中断(比如有更高优先级任务进来)
    if (shouldYield()) {
      // 记录当前进度,下次从这里继续
      nextUnitOfWork = currentFiber;
      return; // 中断遍历
    }

    // 3. 确定下一个要处理的节点(链表遍历逻辑)
    if (currentFiber.child) {
      // 有子节点,先处理子节点
      currentFiber = currentFiber.child;
    } else if (currentFiber.sibling) {
      // 没有子节点,处理兄弟节点
      currentFiber = currentFiber.sibling;
    } else {
      // 既没有子节点也没有兄弟节点,回溯到父节点的兄弟节点
      while (currentFiber.return && !currentFiber.return.sibling) {
        currentFiber = currentFiber.return;
      }
      currentFiber = currentFiber.return ? currentFiber.return.sibling : null;
    }
  }

  // 所有节点处理完毕,进入提交阶段(执行 DOM 操作)
  commitRoot();
}

关键逻辑说明:

  • processFiber:处理当前节点的核心逻辑(虚拟 DOM 对比、标记 DOM 操作);
  • shouldYield:检查是否需要中断——React 会通过浏览器的 requestIdleCallbackMessageChannel API,判断主线程是否有空闲时间,或者是否有更高优先级任务进来;
  • 链表遍历顺序:父 → 子 → 兄弟 → 回溯父节点的兄弟,确保遍历覆盖所有节点。

4. 双缓存机制:提升渲染效率(React 19 优化点)

Fiber 架构的另一个核心是“双缓存机制”,简单说就是:React 维护了两棵 Fiber 树——当前树(Current Tree)工作树(WorkInProgress Tree)

  • 当前树:对应当前页面渲染的 DOM 结构,存储着当前的组件状态和 DOM 信息;
  • 工作树:是 React 在后台构建的“备用树”,所有的更新任务(虚拟 DOM 对比、计算 DOM 操作)都在工作树上进行,不会影响当前页面的渲染。

当工作树上的所有任务都处理完毕后,React 会快速“切换”两棵树的角色——让工作树变成新的当前树,当前树变成下一次更新的工作树。这个切换过程非常快,因为它只需要修改一个“根节点指针”,不需要重新创建整个 DOM 树。

React 19 对双缓存的优化点

React 19 主要优化了“工作树构建效率”和“切换时机”:

  • 复用 Fiber 节点:对于没有变化的组件,React 19 会直接复用当前树的 Fiber 节点到工作树,避免重复创建节点,减少内存开销;
  • 延迟切换:如果工作树构建过程中遇到高优先级任务,React 19 会延迟切换树的时机,先处理高优先级任务,确保用户交互更流畅。

双缓存机制流程图

三、React 19 调度系统:智能分配任务,避免卡顿

有了可中断的 Fiber 架构,还需要一个“智能调度系统”来决定:哪个任务先执行?什么时候执行?什么时候中断当前任务? React 19 的调度系统基于“优先级队列”和“浏览器主线程空闲检测”,实现了高效的任务分配。

1. 核心:任务优先级分级(React 19 增强版)

React 19 对任务优先级进行了更精细的分级,确保“紧急任务先执行,非紧急任务延后执行”。核心优先级分为 5 级(从高到低):

  1. Immediate(立即优先级) :最紧急的任务,必须立即执行,不能中断(比如用户输入、点击事件的同步响应);
  2. UserBlocking(用户阻塞优先级) :影响用户交互的任务,需要尽快执行(比如表单输入后的状态更新、按钮点击后的页面反馈);
  3. Normal(正常优先级) :普通任务,可延迟执行,但不能太久(比如普通的状态更新);
  4. Low(低优先级) :低优先级任务,可长时间延迟(比如列表滚动时的非关键更新);
  5. Idle(空闲优先级) :最不重要的任务,只有当主线程完全空闲时才执行(比如日志上报、非关键数据统计)。

2. 案例:优先级调度的实际效果

假设我们有两个任务同时触发:

  • 任务 A:用户输入框输入文字(UserBlocking 优先级);
  • 任务 B:页面底部列表的非关键数据更新(Low 优先级)。

如果没有调度系统,两个任务可能会并行执行,导致输入延迟。而 React 19 的调度系统会:

  1. 优先执行任务 A(UserBlocking 优先级),确保用户输入流畅;
  2. 任务 A 执行完成后,检查主线程是否有空闲时间;
  3. 如果有空闲时间,再执行任务 B(Low 优先级);如果期间又有紧急任务进来,暂停任务 B,先处理紧急任务。

3. 底层实现:如何检测主线程空闲?

React 19 调度系统的核心,是准确判断“主线程是否有空闲时间”,从而决定是否执行低优先级任务、是否中断当前任务。它主要依赖两个浏览器 API:

  • MessageChannel:用于实现“微任务级别的延迟执行”,替代了早期的 requestIdleCallbackrequestIdleCallback 有兼容性问题,且延迟时间不精准);
  • performance.now() :用于精确计算任务执行时间,判断当前任务是否执行过久,是否需要中断。

简化代码模拟调度系统的空闲检测逻辑:

// 模拟 React 19 调度系统的空闲检测
class Scheduler {
  constructor() {
    this.priorityQueue = []; // 优先级队列
    this.isRunning = false; // 是否正在执行任务

    // 使用 MessageChannel 实现精准延迟
    const channel = new MessageChannel();
    this.port1 = channel.port1;
    this.port2 = channel.port2;
    this.port1.onmessage = this.executeTask.bind(this);
  }

  // 添加任务到优先级队列
  scheduleTask(task, priority) {
    this.priorityQueue.push({ task, priority });
    // 按优先级排序(从高到低)
    this.priorityQueue.sort((a, b) => b.priority - a.priority);
    // 如果没有正在执行的任务,触发任务执行
    if (!this.isRunning) {
      this.port2.postMessage('execute');
    }
  }

  // 执行任务
  executeTask() {
    this.isRunning = true;
    const currentTask = this.priorityQueue.shift();
    if (!currentTask) {
      this.isRunning = false;
      return;
    }

    // 记录任务开始时间
    const startTime = performance.now();
    try {
      // 执行任务(这里的 task 就是 Fiber 树的遍历任务)
      currentTask.task();
    } catch (error) {
      console.error('任务执行失败:', error);
    }

    // 检查任务执行时间是否过长(超过 5ms 认为是长任务,需要中断)
    const executionTime = performance.now() - startTime;
    if (executionTime > 5) {
      // 有未完成的任务,下次继续执行
      this.port2.postMessage('execute');
    } else {
      // 任务执行完成,继续执行下一个任务
      this.executeTask();
    }
  }

  // 检查是否需要中断当前任务(供 Fiber 遍历调用)
  shouldYield() {
    // 计算当前主线程是否有空闲时间(简化逻辑)
    const currentTime = performance.now();
    // 假设 16ms 是一帧的时间(浏览器每秒约 60 帧),超过 12ms 认为没有空闲时间
    return currentTime - this.startTime > 12;
  }
}

关键逻辑说明:

  • 任务添加后,会按优先级排序,确保高优先级任务先执行;
  • MessageChannel 触发任务执行,避免阻塞主线程;
  • 通过 performance.now() 计算任务执行时间,超过阈值(比如 5ms)就中断,下次再继续,避免长时间占据主线程。

4. React 19 调度系统的优化点

React 19 在调度系统上的核心优化的点:

  • 优先级预测:根据历史任务执行情况,预测下一个可能的高优先级任务,提前预留主线程时间;
  • 任务合并:将短时间内触发的多个相同优先级的任务合并为一个,减少重复计算;
  • 低优先级任务防饿死:为低优先级任务设置“过期时间”,如果超过过期时间还没执行,自动提升优先级,确保任务最终能执行。

四、React 19 完整更新流程:Fiber + 调度协同工作

了解了 Fiber 架构和调度系统的核心后,我们把它们结合起来,看看 React 19 处理一次状态更新的完整流程。用流程图和步骤说明,让整个逻辑更清晰。

完整流程流程图

案例:React 19 处理一次点击更新的完整过程

我们以“点击按钮修改 count 状态”为例,拆解整个流程:

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      点击计数:{count}
    </button>
  );
}
  1. 用户点击按钮,触发 onClick 事件,调用 setCount(1);
  2. 调度系统创建更新任务,判断该任务是“用户阻塞优先级”(UserBlocking),加入优先级队列;
  3. 调度系统发现当前没有正在执行的任务,触发任务执行;
  4. Fiber 架构基于当前 Fiber 树(count=0)创建工作树,遍历 Counter 组件对应的 Fiber 节点;
  5. 处理 Counter 节点:对比虚拟 DOM(发现 count 从 0 变为 1),标记“更新文本内容”的 DOM 操作;
  6. 检查中断:任务执行时间很短(不足 1ms),不需要中断;
  7. 工作树构建完成,切换当前树和工作树的角色;
  8. 提交阶段:执行 DOM 操作,将按钮文本从“点击计数:0”更新为“点击计数:1”;
  9. 任务完成,调度系统检查优先级队列,没有其他任务,结束流程。

五、实战避坑:基于 Fiber 架构的性能优化建议

了解了 React 19 的底层原理后,我们可以针对性地做一些性能优化,避免踩坑。核心优化思路是“减少不必要的任务、降低任务优先级、避免长时间占用主线程”。

1. 避免不必要的渲染(减少 Fiber 树遍历范围)

Fiber 树遍历的节点越多,任务耗时越长。我们可以通过以下方式减少不必要的渲染:

  • 使用 React.memo 缓存组件:对于 props 没有变化的组件,避免重新渲染;
  • 使用 useMemo 缓存计算结果:避免每次渲染都执行复杂计算;
  • 使用 useCallback 缓存函数:避免因函数重新创建导致子组件 props 变化。
// 示例:使用 React.memo 缓存组件
const Child = React.memo(({ name }) => {
  console.log('Child 渲染');
  return <p>{name}</p>;
});

function Parent() {
  const [count, setCount] = useState(0);
  // 使用 useCallback 缓存函数
  const handleClick = useCallback(() => {}, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>计数:{count}</button>
      <Child name="小明" onClick={handleClick} />
    </div>
  );
}

优化后,点击按钮修改 count 时,Child 组件不会重新渲染,减少了 Fiber 树的遍历范围。

2. 拆分长任务(避免长时间占用主线程)

如果有复杂的计算任务(比如处理大量数据),不要在组件渲染或 useEffect 中同步执行,否则会占据主线程,导致卡顿。可以用 setTimeout 或 React 18+ 的 useDeferredValue 将任务拆分成小任务,降低优先级。

// 示例:使用 useDeferredValue 降低任务优先级
function DataList() {
  const [data, setData] = useState([]);
  // 延迟处理数据,降低优先级
  const deferredData = useDeferredValue(data);

  // 处理大量数据(复杂任务)
  useEffect(() => {
    const fetchData = async () => {
      const res = await fetch('/api/large-data');
      const largeData = await res.json();
      setData(largeData);
    };
    fetchData();
  }, []);

  // 渲染延迟处理后的数据
  return (
    <ul>
      {deferredData.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

使用 useDeferredValue 后,数据处理任务会被标记为低优先级,不会影响用户交互等紧急任务。

3. 避免在渲染阶段执行副作用

组件渲染阶段(函数组件执行过程)是 Fiber 树遍历的核心阶段,这个阶段的任务必须是“纯函数”(没有副作用)。如果在渲染阶段执行副作用(比如修改 DOM、发送请求、修改全局变量),会导致 Fiber 树构建混乱,且可能延长任务执行时间。

错误示例(渲染阶段执行副作用):

// 错误:渲染阶段修改 DOM
function BadComponent() {
  const divRef = useRef(null);
  // 渲染阶段执行副作用(修改 DOM 文本)
  if (divRef.current) {
    divRef.current.textContent = '渲染中...';
  }
  return <div ref={divRef}></div>;
}

正确做法:将副作用放在 useEffect 中:

// 正确:useEffect 中执行副作用
function GoodComponent() {
  const divRef = useRef(null);
  useEffect(() => {
    // 副作用放在 useEffect 中,在渲染完成后执行
    if (divRef.current) {
      divRef.current.textContent = '渲染完成';
    }
  }, []);
  return <div ref={divRef}></div>;
}

六、核心总结

今天我们从底层原理到实战优化,完整拆解了 React 19 的 Fiber 架构和调度系统。核心要点总结如下:

  1. Fiber 架构的核心价值:将不可中断的递归遍历拆分为可中断、可恢复的链表遍历,避免长任务占据主线程导致卡顿;
  2. React 19 Fiber 升级点:精简 Fiber 节点结构、增强优先级字段精度、优化双缓存切换效率、复用无变化节点;
  3. 调度系统的核心逻辑:基于优先级队列分配任务,通过 MessageChannel 和 performance.now() 检测主线程空闲时间,确保紧急任务先执行;
  4. 协同工作流程:触发更新 → 调度任务 → 构建 Fiber 工作树(可中断) → 切换双缓存树 → 提交 DOM 操作;
  5. 实战优化建议:减少不必要渲染、拆分长任务、避免渲染阶段执行副作用。

七、进阶学习方向

如果想进一步深入 React 19 底层原理,可以重点学习以下内容:

  • Fiber 架构的“提交阶段”细节:如何批量执行 DOM 操作、如何处理副作用;
  • React 19 的“自动批处理”(Automatic Batching):如何合并多个更新任务,减少 Fiber 树构建次数;
  • 并发渲染(Concurrent Rendering):如何基于 Fiber 架构实现“并发更新”,让多个更新任务并行处理;
  • React 源码阅读:重点看 react-reconciler 包(Fiber 架构核心)和 scheduler 包(调度系统核心)。

如果这篇文章对你有帮助,欢迎点赞、收藏、转发~ 有任何问题也可以在评论区留言交流~ 我们下期再见!

浅析-AI时代对前端工程师的影响

作者 EB_Coder
2026年1月19日 08:09

随着AI时代的来临,身边的前端圈子里,“AI”成了茶余饭后的高频词。有人兴奋地展示用最新AI工具生成的精美界面,也有人开始担忧——这些能写代码、能画图的工具,会不会有一天把我们给替代了?

我最初也有类似的困惑,但在慢慢接触了一段时间后,我想分享一些不同的观察。这并不是那种充斥着“神经网络”、“深度学习”术语的技术文章,而是一个普通前端开发者对AI最朴素、浅层的的理解。(如果你是一名AI的资深大佬,那么你大可不必浪费时间在这篇文章上)

一、不要把AI想得有多么的“高大上”

首先,我们可以先忘掉那些复杂的定义。在我看来,AI更像是一个非常擅长找规律的学生

举个例子,你给一个小孩看了一千张老虎的图片,每次都告诉他“这是老虎”。下次你再给他看一张他从未见过的老虎图片,他也大概率能认出来。其实AI做的事情本质上和这个很类似——通过分析海量的现有代码、设计稿、用户行为数据,它逐渐学会了前端开发中常见的“套路”。

即使它没见过真正的逻辑,但它记住了那些频繁出现的模式:比如一个登录页面通常有用户名输入框、密码输入框和提交按钮;一个电商商品卡片通常包含图片、标题、价格和购买按钮。当你让它“生成一个登录页面”时,它不过是在回忆并组合这些它之前见过的模式片段。

所以,AI既不是魔法,更不是万能的神。它的“智能”建立在它见过的数据之上,它擅长处理常规的、模式化的任务,但对于全新的、需要深度逻辑推理或创造性突破的工作,它仍然力不从心。

二、AI正在如何改变前端

这种“善于找规律”的特性,已经开始渗透到前端开发的各个环节。这些变化不是翻天覆地的革命,更像是润物无声的升级:

1. 从“重复建造”到“定制调整”

以前,我们从零开始搭建一个基础的管理后台表格,需要手动处理表格框架、分页逻辑、筛选排序等功能,是相当耗时的。现在,你可以直接对AI说:“生成一个带分页、搜索和批量操作的用户数据表格,用ReactAnt Design。”它能在几秒内给你一份可工作的基础代码。你的角色,从重复的“搬砖仔”,变成了更具主导性的“建筑师”或“装修师”,以前重复的“搬砖”时间可以用在专注于根据具体业务需求进行深度定制、优化性能和用户体验方面。

2. 设计到代码的“翻译”过程正在加速

“设计稿切图”这个经典环节正在被重塑。通过一些AI工具,你可以上传UI设计稿(Figma、Sketch等),它能自动识别组件、布局和样式,生成结构清晰、语义化的HTML/CSS代码骨架。虽然目前还无法做到100%完美(尤其对于复杂或高度自定义的交互),但它极大地压缩了视觉稿到初始代码之间的机械转换时间,让我们能更早地进入真正的逻辑开发和交互实现阶段。

3. 开发副驾驶已就位

最直接的体验,可能就是代码补全和智能提示的进化。比如像GitHub Copilot这样的工具,已经能根据你的代码上下文,预测并建议一整行甚至一个完整的函数。当你写一个日期格式化函数时,它可能会自动补全出整个逻辑。这就像一个时刻在线的、熟悉你项目习惯的助手,帮你处理那些琐碎的语法和常见代码片段,让你更专注于业务逻辑本身。

4. 个人效率工具的普及

除了写代码,AI还能成为我们工作流中的多方面的助手:用自然语言让它帮你编写测试用例的描述、生成模拟数据(“给我20条包含姓名、城市和订单金额的用户数据”)、优化代码注释、甚至解释一段复杂的遗留代码。这些工具正在将我们从大量辅助性、文档性工作中解放出来,从而我们有更多的时间去关注业务本身。

三、前端开发者,可以这样开始

面对这些变化,焦虑是没有用的,关上大门更不明智。主动了解和学习,把它变成我们的“副驾驶”,才是更务实的态度。你可以尝试从以下几个非常具体的步骤开始:

第一步:先感受,再深究

你完全不必一开始就去啃机器学习教科书。最好的入门方式就是直接用起来

  • 玩玩ChatGPT(或类似产品):不要只把它当聊天机器人。试着向它提问前端相关的问题:
    • “用JavaScript写一个深度拷贝函数,并解释原理。”

    • Vue 3Composition APIOptions API主要区别是什么?”

    • “帮我优化这段CSS,让它更高效。”

  • 在编辑器中尝试Copilot:如果你使用VSCode,可以安装GitHub Copilot的扩展。在写代码时,观察它的建议,接受有用的代码,思考它为什么这么建议。这是最直接的、沉浸式的体验。

第二步:建立“AI思维”

在使用过程中,刻意培养一种新的工作思路:

  • 学会提问AI的输出质量,很大程度上取决于你的输入。练习如何清晰、具体地描述你的需求。比如,从“写个表格”升级到“用React函数式组件写一个可排序、可分页的用户数据表格,使用Tailwind CSS样式”。

  • 成为审批者,而非搬运工:永远不要无脑的复制AI生成的代码。把它看作一个才华横溢但有时会犯错的实习生。你的核心职责是审查、测试和整合。仔细检查它的逻辑是否正确,是否存在安全漏洞(如XSS风险),代码风格是否符合项目规范,性能是否最优。

第三步:深化你的核心护城河

恰恰因为AI擅长处理模式化的任务,我们更应该强化那些它不擅长的能力(至少目前是这样),这些是我们的核心竞争力:

  • 更深的业务理解:只有你深入理解产品的业务逻辑、用户的实际痛点,才能判断AI生成的方案是否真的能解决问题,并指导AI朝正确的方向改进。

  • 更优的架构设计:如何设计可维护、可扩展、高性能的前端架构,如何做技术选型,这些全局性的、高层次的思考,至少目前来说AI是难以替代的。

  • 极致的用户体验:对交互动效的细腻把控程度,对无障碍访问(A11y)的深入实践,对用户心理和行为的洞察,这些创造性和同理心驱动的领域,依然是人类开发者的舞台。

  • 复杂的交互与逻辑:处理非常规的、状态复杂的交互流程(如一个多步骤的、有大量条件分支的表单向导),编写高度定制化的动画和视觉效果,这些依然是我们的强项。

写在最后

AI不是来取代前端开发者的,它更像是一个强大的、不知疲倦的杠杆。它把我们从大量重复劳动中托举出来,让我们有机会站在更高的地方,去解决更复杂、更有价值的问题。

这场变化的关键,不在于我们会不会被淘汰,而在于我们选择如何与这个新工具“共舞”。是把它关在门外,守着旧工具渐渐落伍;还是走上前,握住它,让自己原本需要一天完成的枯燥工作,压缩到一小时,然后用省下的时间,去钻研动画原理、去优化渲染性能、去理解业务本质,成为一个更不可替代的专家?

仁者见仁,智者见智,答案在我们自己手中。从此刻起,尝试着向AI提出你的第一个问题吧!这或许是你能为未来的自己,做的最有价值的投资之一。

❌
❌