☰
React Native FlashList 测试指南:用 Jest 模拟布局测量,为高性能列表编写单元测试
2026/10/8 1:28:56 网站建设 项目流程
  • 移动开发
  • UI组件
  • 跨平台

【免费下载链接】flash-list

A better list for React Native

项目地址:https://gitcode.com/gh_mirrors/fl/flash-list
点击查看免费下载

FlashList 是 React Native 平台上 FlatList 的高性能替代方案,其核心渲染机制建立在"延迟挂载 + 布局测量"之上:组件不会立即渲染列表项,而是先测量底层ScrollView的尺寸,再按窗口大小决定挂载哪些 item。这一机制在 Jest 的 jsdom 测试环境中无法自然触发,因此 FlashList 官方提供了专门的 Jest 预设,通过 mock 布局测量函数让列表在测试中"立即有尺寸可用"。本文基于 FlashList 1.x 官方测试文档,结合仓库源码,完整讲解 Jest 测试环境的搭建步骤、jestSetup.js的底层原理、基于@testing-library/react-native的测试写法,以及 FlashList 仓库自身的测试实践,帮助你为自己的列表组件编写可靠、可复现的单元测试。

为什么 FlashList 在测试环境中需要特殊处理

普通 React Native 组件在 Jest 中可以直接渲染,但 FlashList 走的是另一条路径。在官方 1.x 测试文档中,作者明确给出了原因:

SinceFlashListdoes not immediately render but waits for the size of the underlyingScrollView(unless you specifyestimatedListSize), we need to mock triggeringonLayoutevent.

翻译过来就是:FlashList 并不会立刻渲染列表项,而是等待底层ScrollView的尺寸确定后才开始挂载 item——除非你显式传入了estimatedListSize属性。因此,在测试环境中我们需要 mock 掉"触发onLayout事件"这一环节。

从1.x 使用文档可以查到estimatedListSize的定义:

estimatedListSize?: { height: number; width: number }

它是列表可见区域的估算宽高(注意不是滚动内容的总尺寸)。显式指定该属性后,列表可以在首帧立即渲染,无需先测量自身尺寸;不指定时,列表必须先完成一次测量,导致首次渲染存在微小延迟。在测试环境(jsdom)中,真实的measureLayout调用无法返回有效值,所以"等待测量"会让列表永远挂载不出任何 item——这正是需要 mock 的根因。

版本提示:estimatedListSize是 FlashList 1.x 的属性。从仓库的 v2-changes.md 看,v2 中该属性已不再使用;如果按本文配置 mock 方案,则无需依赖该属性。

官方 Jest 预设:jestSetup.js的工作原理

FlashList 在包根目录提供了现成的 Jest 预设文件 jestSetup.js,并在 package.json 的files字段中明确包含它("jestSetup.js"),确保发布到 npm 后用户可以直接引用。完整内容如下:

jest.mock("@shopify/flash-list/dist/recyclerview/utils/measureLayout", () => { const originalModule = jest.requireActual( "@shopify/flash-list/dist/recyclerview/utils/measureLayout" ); return { ...originalModule, measureParentSize: jest.fn().mockImplementation(() => ({ x: 0, y: 0, width: 400, height: 900, })), measureFirstChildLayout: jest.fn().mockImplementation(() => ({ x: 0, y: 0, width: 400, height: 900, })), measureItemLayout: jest.fn().mockImplementation(() => ({ x: 0, y: 0, width: 100, height: 100, })), }; });

这段代码的核心动作是:用jest.mock替换measureLayout模块中的三个关键函数,让它们无论何时被调用,都返回固定的"测量结果"。其设计意图与源码注释完全对应——在 src/recyclerview/utils/measureLayout.ts 中,这三个函数都被明确标注为"方便 mock":

  • measureParentSize(view):测量 RecyclerView 外层容器的尺寸。源码注释写着 "Specific method for easier mocking",内部通过measureLayoutRelative(view, view, undefined)同步拿到{ width, height }。mock 后固定返回400 × 900,相当于模拟了一个 400 宽、900 高的列表窗口。
  • measureFirstChildLayout(childContainerView, parentView):测量子容器相对父容器的布局,用于推算第一个 item 的偏移量(x/y坐标)。mock 后固定返回{ x: 0, y: 0, width: 400, height: 900 },即子容器与父容器完全重合,首项偏移为 0。
  • measureItemLayout(item, oldLayout):测量单个列表项的布局,是回收复用机制判断"该项尺寸是否变化"的依据。mock 后每个 item 固定为100 × 100,让布局管理器可以确定每屏能放多少个 item。

正是因为这三个函数返回了确定值,FlashList 的"等待onLayout"环节被彻底绕开:RecyclerView挂载后立刻拿到窗口尺寸与 item 尺寸,随即开始挂载首屏 item。这就是require("@shopify/flash-list/jestSetup")一行代码解决测试环境渲染问题的完整原理。

配置 Jest 测试环境

第一步:创建 jest-setup.js 并引入官方预设

在你的项目根目录新建(或打开已有的)jest-setup.js文件,加入一行:

require("@shopify/flash-list/jestSetup");

这行代码会在每个测试文件执行前运行,完成对measureLayout模块的全局 mock。

第二步:检查 jest.config.js 配置

接着确认你的 jest.config.js(或 package.json 中的jest字段)包含以下两个关键项:

... preset: 'react-native', setupFiles: ['./jest-setup.js'], ...
  • preset: 'react-native':使用 React Native 官方 Jest 预设,负责处理 RN 组件、react-native模块等原生模块的转换与 mock(仓库自身的 jest.config.js 同样使用该 preset)。
  • setupFiles: ['./jest-setup.js']:在测试框架安装之前执行 setup 文件,确保 mock 先于测试代码生效。注意这里是setupFiles而不是setupFilesAfterEach,顺序不同会导致 mock 时机错误。

配置完成后运行jest(仓库中对应的脚本是 package.json 里的"test": "jest"),列表即可在测试环境中正常渲染。

编写第一个组件测试

完成环境配置后,就可以用@testing-library/react-native编写测试了。以下是官方文档给出的完整示例:

import React from "react"; import { render } from "@testing-library/react-native"; describe("MyFlashListComponent", () => { it("renders items", () => { const { getByText } = render(<MyFlashListComponent />); const element = getByText("Title of one of the items"); // Do something with element ... }); });

要点说明:

  • render(<MyFlashListComponent />)会挂载你的列表组件。得益于jestSetup.js的 mock,列表不再"卡在等待测量"阶段,而是立即按400 × 900的窗口尺寸、100 × 100的 item 尺寸挂载首屏 item。
  • getByText("Title of one of the items")用于断言列表渲染出了指定内容的 item,你可以继续用返回的element做交互模拟、样式断言等后续验证。
  • 如果首屏(按 mock 尺寸计算:900 / 100 ≈ 9 行)没有渲染出你查找的 item,请检查它是否真的排在列表头部,或者需要配合scrollTo/onViewableItemsChanged等能力滚动后再断言。

深入原理:真实环境中的布局测量链路

理解了 mock 方案后,再回到真实运行环境中看这三个函数被谁调用、如何影响渲染,能让你在排查"为什么测试和真机行为不一致"时更有把握。

在 src/recyclerview/RecyclerView.tsx 中,组件挂载时通过useLayoutEffect执行初始化测量:

useLayoutEffect(() => { if (internalViewRef.current && firstChildViewRef.current) { const outerViewSize = measureParentSize(internalViewRef.current); const firstChildViewLayout = measureFirstChildLayout( firstChildViewRef.current, internalViewRef.current ); containerViewSizeRef.current = outerViewSize; // firstChildViewLayout 已是相对外层容器的坐标,其 x/y 直接给出首项偏移 const firstItemOffset = horizontal ? firstChildViewLayout.x : firstChildViewLayout.y; recyclerViewManager.updateLayoutParams( { width: horizontal ? outerViewSize.width : firstChildViewLayout.width, height: horizontal ? firstChildViewLayout.height : outerViewSize.height, }, // RTL 横向列表需要额外换算偏移 isHorizontalRTL && recyclerViewManager.hasLayout() ? firstItemOffset - recyclerViewManager.getChildContainerDimensions().width : firstItemOffset ); } });

可以看到,measureParentSize与measureFirstChildLayout的结果直接决定了updateLayoutParams拿到的窗口尺寸和首项偏移——这是布局管理器计算"该挂载哪些 item"的起点。而measureItemLayout则用于后续每个 item 的尺寸更新与回收复用判定。因此 mock 这三个函数,本质上就是把"测量"这一步从真实 DOM 操作替换为固定值,从而让整条渲染链路在 Jest 中顺畅运转。

真实环境中,measureLayout.ts 的实现依赖 React Native 的view.measureLayout()原生方法,并借助PixelRatio.roundToNearestPixel消除浮点精度误差(areDimensionsEqual允许 1px 容差)。Jest 环境无法执行这些原生调用,这正是官方提供预设而不是让用户手动 mock 的原因。

FlashList 仓库自身的测试实践

FlashList 仓库本身就是这套 mock 方案的最大用户,可以作为你编写测试时的参考范本。

在 src/tests/RecyclerView.test.tsx 中,仓库同样在文件顶部 mock 了measureLayout(窗口尺寸 mock 为399 × 899、item 为100 × 100),然后断言首屏渲染行为:

jest.mock("../recyclerview/utils/measureLayout", () => { const originalModule = jest.requireActual( "../recyclerview/utils/measureLayout" ); return { ...originalModule, measureParentSize: jest.fn().mockImplementation(() => ({ width: 399, height: 899, })), measureFirstChildLayout: jest.fn().mockImplementation(() => ({ x: 0, y: 0, width: 399, height: 899, })), measureItemLayout: jest.fn().mockImplementation(() => ({ x: 0, y: 0, width: 100, height: 100, })), }; }); it("renders items", () => { const result = renderRecyclerView({}); expect(result).toContainReactComponent(Text, { children: 0 }); expect(result).not.toContainReactComponent(Text, { children: 11 }); });

这个测试非常直观地验证了回收渲染的核心语义:按 mock 尺寸计算,首屏只会挂载约 9 个 item,因此children: 0的 item 存在,而超出首屏的children: 11不应被渲染。同样的 mock 模式还出现在 LayoutCommitObserver.test.tsx、StickyHeaders.test.tsx 等测试文件中,你可以按需查阅。

仓库的 jest.config.js 还配置了transformIgnorePatterns(放行react-native相关包)与自定义resolver(shared/testing/resolver.js),如果你在接入 FlashList 后遇到模块解析问题,可以参考这些配置调整自己的 Jest 设置。

常见问题与注意事项

  • mock 尺寸如何影响测试断言:官方预设将窗口固定为400 × 900、item 固定为100 × 100,即"一屏约 9 个 item"。若你的业务测试依赖更多 item 可见,可以在自己的jest-setup.js中复制官方 jestSetup.js 的写法,自行调整返回的宽高数值(参照仓库测试中399 × 899的做法)。
  • 为什么要用setupFiles:只有setupFiles会在测试框架安装前执行jest.mock,如果误配到setupFilesAfterAll,mock 将不会生效,列表依旧无法渲染。
  • v1 与 v2 的差异:本文配置基于 FlashList 1.x 官方测试文档;当前仓库版本为 2.3.3,estimatedListSize在 v2 已废弃(见 v2-changes.md 与 v2-migration.md),但 mockmeasureLayout的核心思路在 v2 的当前测试文档中同样适用。

总结

FlashList 的 Jest 测试方案可以归纳为一条主线:列表渲染依赖布局测量,测试环境无法执行真实测量,因此用固定返回值 mock 测量函数。具体落地只需三步:创建jest-setup.js并require("@shopify/flash-list/jestSetup")、在jest.config.js中配置preset: 'react-native'与setupFiles、然后用@testing-library/react-native的render+getByText断言首屏 item 的渲染结果。理解了measureParentSize/measureFirstChildLayout/measureItemLayout三个函数在渲染链路中的作用(见 RecyclerView.tsx 的初始化测量逻辑),你就能举一反三:既可以按需定制 mock 尺寸,也能在遇到"测试能渲染但行为异常"时快速定位到是测量值不符合预期,还是渲染窗口设置不当。

  • 移动开发
  • UI组件
  • 跨平台

【免费下载链接】flash-list

A better list for React Native

项目地址:https://gitcode.com/gh_mirrors/fl/flash-list
点击查看免费下载
上一篇:Lo-Fi Player开发解析:Tone.js与Web Audio API如何实现高品质音频合成
下一篇:OpenClaw Tavily 插件实战:为 Agent 接入结构化网页搜索与 URL 内容提取

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询