- 移动开发
- UI组件
- 跨平台
【免费下载链接】flash-list
A better list for React Native
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 测试文档中,作者明确给出了原因:
Since
FlashListdoes 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
相关推荐
Tendis集群管理完全手册:从零搭建千节点分布式系统
Tendis集群管理完全手册:从零搭建千节点分布式系统 Tendis作为一款高性能分布式存储系统,完全兼容Redis协议,为大规模数据存储提供了可靠解决方案。本
数据库KV存储分布式数据库深入源码解析lambda_resnet26rpt_256.c1_in1k:timm库中的实现细节
深入源码解析lambda_resnet26rpt_256.c1_in1k:timm库中的实现细节 lambda_resnet26rpt_256.c1_in1k是
tui.editor单元测试模拟:使用Jest测试编辑器功能
tui.editor单元测试模拟:使用Jest测试编辑器功能 在前端开发中,单元测试是确保代码质量的关键环节。对于富文本编辑器这类复杂组件,测试环境的配置尤为重
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考