news 2026/10/16 6:24:50

仓颉语言 ArkUI 开发指南:ForEach 循环渲染的键值机制、使用场景与避坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
仓颉语言 ArkUI 开发指南:ForEach 循环渲染的键值机制、使用场景与避坑实践
  • 文档
  • 知识库
  • 教程
  • OpenHarmony

【免费下载链接】CangjieCorpus

本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!

项目地址:https://gitcode.com/Cangjie/CangjieCorpus
点击查看免费下载

ForEach 是 ArkUI 声明式 UI 中基于数组数据源进行循环渲染的核心接口,它通过与容器组件(如 List、Grid、Column 等)配合,把数据数组批量转换为可交互的 UI 组件树。本文以仓颉(Cangjie)语言实现为例,系统讲解 ForEach 的键值生成规则、首次与非首次渲染流程、两类典型使用场景(数据源不变 / 数据源数组项变化),并结合完整可运行示例剖析"键值规则包含 index"和"缺省 keyGenerator"两类不推荐写法背后的渲染原理与性能代价,帮助开发者在鸿蒙应用中写出正确且高效的循环渲染代码。

ForEach 基本用法与适用边界

ForEach 接口基于数组类型数据做循环渲染,必须与容器组件配合使用,且接口返回的组件应当是允许包含在 ForEach 父容器组件中的子组件。例如 ListItem 组件 要求 ForEach 的父容器必须为 List 组件;同理,Grid、Swiper 等容器内的 ForEach 也要保证生成的子组件类型合法。

从 渲染控制概述 可知,ArkUI 的渲染控制语句还包括条件渲染、面向大数据量的数据懒加载(LazyForEach)等。ForEach 与之互补:它是"全量渲染"模型,适用于数据量中等、组件结构简单的场景;当列表项数量很大时,应评估使用 LazyForEach 结合缓存、预加载、组件复用等手段提升滑动帧率、降低内存占用。在 List、Grid、Swiper 内使用 ForEach 时,不要与 LazyForEach 混用,同一容器内同时包含 ForEach 与 LazyForEach 是不推荐的写法。

ForEach 的典型调用形态如下(以仓颉代码为例):

ForEach(this.simpleList, itemGeneratorFunc: {item: String, idx: Int64 => ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 => return item})

其中第二个参数itemGeneratorFunc负责为每个数组项创建组件,第三个参数keyGeneratorFunc负责为每个数组项生成键值,这两个参数都接收(item, index)两个入参。

键值生成规则:唯一且持久的组件标识

在 ForEach 循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,仓颉将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。

ForEach 提供名为keyGenerator(仓颉代码中写作keyGeneratorFunc)的函数参数,开发者可以通过它自定义键值的生成规则;若开发者没有定义该函数,则 ArkUI 框架会使用默认的键值生成函数。

ArkUI 框架对于 ForEach 的键值生成有一套特定的判断规则,这套规则主要与itemGeneratorFunc的第二个参数index以及keyGeneratorFunc的第二个参数index有关。理解这层关系是正确使用 ForEach 的前提:

  • 默认键值生成规则:缺省keyGenerator时,框架会以"index 前缀 + 数组项内容"组合生成键值(如0__one、1__two、2__three)。由于默认规则包含索引,数据源一旦在中间位置插入/删除项,后续所有项的键值都会变化,触发大范围组件重建。
  • 自定义键值规则:开发者可基于数组项自身的唯一字段(如对象的id)生成稳定键值,使键值与索引解耦,从而在数据项增删时最大化组件复用。

说明:

ArkUI 框架会对重复的键值发出警告。在 UI 更新的场景下,如果出现重复的键值,框架可能无法正常工作,具体表现可参见下文 渲染结果非预期 一节。

组件创建规则与首次渲染

在确定键值生成规则后,itemGeneratorFunc会根据键值生成规则为数据源的每个数组项创建组件。组件的创建分两种情况:首次渲染与非首次渲染。

在 ForEach 首次渲染时,系统根据键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。下面是一个最简示例:数据源为三个不重复的字符串,键值规则直接返回item。

package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* @Component class ChildItem { @Prop var item: String func build() { Text(this.item) .fontSize(50) } } @Entry @Component class EntryView { @State var simpleList: Array<String> = ['one', 'two','three'] func build() { Row() { Column() { ForEach(this.simpleList,itemGeneratorFunc: {item: String,idx:Int64 => ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 => return item}) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) } .height(100.percent) .backgroundColor(Color.White) } }

在上述代码中,键值生成规则是keyGeneratorFunc的返回值item。ForEach 在渲染循环时,为数据源数组项依次生成键值one、two和three,并创建对应的ChildItem组件渲染到界面上。

需要特别警惕的是:当不同数组项按照键值生成规则生成的键值相同时,框架的行为是未定义的。例如把数据源改为['one', 'two', 'two', 'three'],ForEach 渲染相同的数据项two时只创建了一个ChildItem组件,而不会创建多个具有相同键值的组件:

package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* @Entry @Component class EntryView { @State var simpleList: Array<String> = ['one', 'two','two','three'] func build() { Row() { Column() { ForEach(this.simpleList,itemGeneratorFunc: {item: String,idx:Int64 => ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 => return item}) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) } .height(100.percent) .backgroundColor(Color.White) } }

该示例中最终键值生成规则为item。当 ForEach 遍历数据源simpleList,遍历到索引为 1 的two时,生成键值two并创建组件、进行标记;当遍历到索引为 2 的two时,按规则生成的键值仍为two(已存在),此时不再创建新的组件。因此界面只渲染出一个two,与数据源中出现的两次two不匹配——这是开发者自定义键值规则时必须规避的重复键值陷阱。

非首次渲染:键值复用决定组件复用

在 ForEach 组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在:

  • 键值不存在→ 创建新的组件;
  • 键值已存在→ 不创建新组件,直接渲染该键值所对应的既有组件(组件被复用)。

下面的示例通过点击事件修改数组的第三项值为"new three",从而触发 ForEach 进行非首次渲染:

package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* @Component class ChildItem { @Prop var item: String func build() { Text(this.item) .fontSize(50) } } @Entry @Component class EntryView { @State var simpleList: ObservedArrayList<String> = ObservedArrayList<String>(['one', 'two','three']) func build() { Row() { Column() { Text("点击修改第3个数组项的值") .fontSize(24) .fontColor(Color.Red) .onClick({evt=> this.simpleList[2] = 'new three' }) ForEach(this.simpleList,itemGeneratorFunc: {item: String,idx:Int64 => ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 => return item}) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) } .height(100.percent) .backgroundColor(Color.White) } }

此例把数据源从Array换成ObservedArrayList,这背后依赖的是 @State 状态管理宏:@State能够监听到简单数据类型数组数据源simpleList数组项的变化。整个非首次渲染过程可拆解为三步:

  1. 当simpleList数组项发生变化时,触发 ForEach 进行重新渲染;
  2. ForEach 遍历新的数据源['one', 'two', 'new three'],并生成对应的键值one、two和new three;
  3. 键值one、two在上次渲染中已存在,因此 ForEach 复用对应组件并渲染;第三个数组项"new three"按规则生成的键值new three在上次渲染中不存在,因此 ForEach 为该数组项创建了一个新组件。

由此可见,键值是否稳定,直接决定了数据变更时组件是"复用"还是"重建"——这正是 ForEach 性能调优的核心抓手。

使用场景一:数据源不变(骨架屏等静态渲染)

ForEach 的主要应用场景包括两类:数据源不变、数据源数组项发生变化(如插入、删除操作)。

在数据源保持不变的场景中,数据源可以直接采用基本数据类型。例如页面加载状态时使用骨架屏列表进行渲染展示,此时数据只用于控制"渲染多少个占位块",内容本身不参与业务变化:

package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* @Builder func textArea(width: Int64, height:Int64) { Row() .width(width) .height(height) .backgroundColor(Color.White) } @Component class ArticleSkeletonView { func build() { Row() { Column() { textArea(80, 80) } .margin(right: 20 ) Column() { textArea(60, 20) textArea(50, 20) } .alignItems(HorizontalAlign.Start) .justifyContent(FlexAlign.SpaceAround) .height(100) } .padding(20) .borderRadius(12) .backgroundColor(Color.Gray) .height(120) .width(100.percent) .justifyContent(FlexAlign.SpaceBetween) .margin(top: 20) } } @Entry @Component class EntryView { @State var simpleList: Array<Int64> = [1, 2, 3, 4, 5] func build() { Column() { ForEach(this.simpleList, itemGenerator: {item: Int64,idx:Int64 =>ArticleSkeletonView()} ) } .padding(20) .width(100.percent) .height(100.percent) } }

这里@Builder宏把骨架屏的占位区域(textArea)封装为可复用片段,ForEach依据simpleList(5 个整数)生成 5 个ArticleSkeletonView占位卡片。由于数据源在此场景下从不变化,基本数据类型直接充当数组项是安全且简洁的选择。

使用场景二:数据源数组项发生变化(列表上拉加载更多)

在数据源数组项发生变化的场景下(数组插入、删除操作,或数组项索引位置发生交换),数据源应为对象数组类型,并使用对象的唯一 ID 作为最终键值。典型例子是页面通过手势上滑加载下一页数据:在数据源数组尾部追加新获取的数据项,使数据源数组长度增大。

package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* import std.collection.ArrayList import kit.LocalizationKit.* import kit.PerformanceAnalysisKit.Hilog import ohos.resource.__GenerateResource__ class Article { var id: String var title: String var brief: String init(id: String, title: String, brief: String) { this.id = id this.title = title this.brief = brief } } @Builder func textArea(width: Int64, height:Int64) { Row() .width(width) .height(height) .backgroundColor(Color.White) } @Component class ArticleCard { @Prop var article: Article func build() { Row(){ // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 Image(@r(app.media.startIcon)) .width(80) .height(80) .margin(right:20) Column() { Text(this.article.title) .fontSize(20) .margin(bottom:8) Text(this.article.brief) .fontSize(16) .fontColor(Color.Gray) .margin(bottom: 8) } .alignItems(HorizontalAlign.Start) .width(80.percent) .height(100.percent) } .padding(20) .borderRadius(12) .backgroundColor(Color.White) .height(120) .width(100.percent) .justifyContent(FlexAlign.SpaceBetween) .margin(top: 20) } } @Entry @Component class EntryView { @State var isListReachEnd: Bool = false @State var articleList: ObservedArrayList<Article> = ObservedArrayList<Article>([ Article('001', '第1篇文章', '文章简介内容'), Article('002', '第2篇文章', '文章简介内容'), Article('003', '第3篇文章', '文章简介内容'), Article('004', '第4篇文章', '文章简介内容'), Article('005', '第5篇文章', '文章简介内容'), Article('006', '第6篇文章', '文章简介内容') ]) func loadMoreArticles() { this.articleList.append(Article('007', '加载的新文章', '文章简介内容')) Hilog.info(0,"here","here") } func build() { Column(space: 5) { List() { ForEach(this.articleList, itemGeneratorFunc: {item: Article,idx:Int64 => ListItem() { ArticleCard(article: item) } }, keyGeneratorFunc: {item: Article, idx: Int64 => return item.id}) } .onReachEnd( {=> this.isListReachEnd = true }) .padding(20) .scrollBar(BarState.Off) } .width(100.percent) .height(100.percent) .backgroundColor(Color.Gray) } }

在本示例中,ArticleCard组件作为 List 中子组件,通过@Prop装饰器接收一个Article对象,用于渲染文章卡片。其工作流程为:

  1. 当列表滚动到底部(onReachEnd触发)时,调用loadMoreArticles()函数,在articleList数据源尾部append一个新的Article('007', ...),使数据源长度增大;
  2. 数据源被@State装饰器修饰,ArkUI 框架能够感知到数据源长度的变化,并触发 ForEach 进行重新渲染;
  3. 键值规则为item.id,新项'007'是全新键值,ForEach 只需创建这一个新组件,其余 6 个已有卡片全部复用,插入成本最低。

这里要特别说明两点工程细节:

  • Image(@r(app.media.startIcon))中的资源引用仅作示例,开发者必须替换为自己的媒体资源,否则 imageSource 创建失败会导致后续无法正常执行;
  • Hilog.info来自 [kit.PerformanceAnalysisKit.Hilog],用于在真机/DevEco Studio 日志中观察加载行为,实际项目中可改为合理的业务日志。

使用建议:键值规则的设计准则

综合以上原理,ForEach 的键值设计应遵循以下准则:

  • 对象数据用唯一 id 作键值:为满足键值的唯一性,对于对象数据类型,建议使用对象数据中的唯一 id 作为键值,如item.id。
  • 避免在最终键值规则中包含 index:尽量避免在键值生成规则中混入数据项索引index,以防止出现渲染结果非预期和渲染性能降低。如果业务确实需要index(例如列表需要通过 index 进行条件渲染),开发者需要接受 ForEach 在改变数据源后重新创建组件所带来的性能损耗。
  • 基本数据类型防重复:基本数据类型的数据项没有唯一 ID 属性。如果使用基本数据类型本身作为键值,必须确保数组项无重复。因此,对于数据源会发生变化的场景,建议将基本数据类型数组转化为具备唯一 ID 属性的对象数据类型数组,再用唯一 ID 属性作为键值。
  • index 参数的正确角色:在上述限制规则下,index参数存在的意义在于——它是开发者保证键值唯一性的最终手段;同时,由于itemGenerator中的item参数是不可修改的,对数据项进行修改时必须借助index索引值去修改数据源,进而触发 UI 重新渲染。
  • 禁止与 LazyForEach 混用:ForEach 在 List、Grid、Swiper 等容器内使用时,不要与 LazyForEach 混用。以 List 为例,同时包含 ForEach、LazyForEach 的情形是不推荐的。
  • 避免内容相同的新项替换旧项:数组项是对象数据类型的情况下,不建议用内容相同的数组项替换旧的数组项(会造成键值冲突或组件内容无法正确区分)。

不推荐案例一:键值规则包含 index 导致渲染结果非预期

开发者对键值生成规则理解不充分时,错误使用一方面会造成功能层面问题(渲染结果非预期),另一方面会造成性能层面问题(渲染性能降低)。

先看渲染结果非预期的案例。通过设置第三个参数keyGenerator函数,把键值规则自定义为数据源的索引index的字符串类型值;点击父组件中的"在第 1 项后插入新项"按钮后,界面出现非预期结果:

package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* import std.collection.ArrayList import kit.LocalizationKit.* import kit.PerformanceAnalysisKit.Hilog @Component class ChildItem { @Prop var item: String func build() { Text(this.item) .fontSize(30) } } @Entry @Component class EntryView { @State var simpleList: ObservedArrayList<String> =ObservedArrayList(['one', 'two', 'three']) func build() { Column() { Button() { Text('在第1项后插入新项').fontSize(30) } .onClick({ evt => this.simpleList.insert(1,'new item') }) ForEach(this.simpleList, itemGeneratorFunc: {item: String ,idx:Int64 =>ChildItem(item: item) },keyGeneratorFunc: {item: String, index: Int64 => index.toString()} ) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) .backgroundColor(Color.White) } }

我们来逐步还原框架的执行过程:

  1. ForEach 首次渲染时,创建的键值依次为"0"、"1"、"2";
  2. 插入新项后,数据源simpleList变为['one', 'new item', 'two', 'three'],框架监听到 @State 装饰的数据源长度变化,触发 ForEach 重新渲染;
  3. ForEach 依次遍历新数据源:
    • 遍历"one"生成键值"0",已存在 → 不创建新组件(旧组件上渲染的仍是one,恰好匹配);
    • 遍历"new item"生成键值"1",已存在 → 不创建新组件(但旧键值"1"对应的是two,内容不匹配);
    • 遍历"two"生成键值"2",已存在 → 不创建新组件(旧键值"2"对应的是three,内容不匹配);
    • 遍历"three"生成键值"3",不存在 → 创建内容为three的新组件并渲染。

最终:期望的界面渲染结果为['one', 'new item', 'two', 'three'],而实际渲染结果为['one', 'two', 'three', 'three']。其根因是键值绑定在易变的索引上,插入操作使所有后续项的键值整体错位,而 ForEach 只认键值、不认内容。因此,开发者在使用 ForEach 时应尽量避免最终键值生成规则中包含 index。

不推荐案例二:缺省 keyGenerator 导致渲染性能降低

再看渲染性能降低的案例。此例中 ForEach 的第三个参数keyGenerator处于缺省状态,点击"在第 1 项后插入新项"后,ForEach 需要为第 2 个数组项以及其后的所有项重新创建组件:

package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* import std.collection.ArrayList import kit.LocalizationKit.* import kit.PerformanceAnalysisKit.Hilog @Component class ChildItem { @Prop var item: String protected override func aboutToAppear() { Hilog.info(0,"0","[aboutToAppear]: item is ${this.item}") } func build() { Text(this.item) .fontSize(30) } } @Entry @Component class EntryView { @State var simpleList: ObservedArrayList<String> =ObservedArrayList(['one', 'two', 'three']) func build() { Column() { Button() { Text('在第1项后插入新项').fontSize(30) } .onClick({ evt =>this.simpleList.insert(1,'new item') }) ForEach(this.simpleList, itemGenerator: {item: String ,idx:Int64 => ChildItem(item: item)} ) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) .backgroundColor(Color.White) } }

该示例在ChildItem中重写了aboutToAppear()生命周期函数并用Hilog打印日志,从而可以在 DevEco Studio 日志中直观观察到组件的创建行为。插入新项后,ForEach 为new item、two、three三个数组项都创建了对应组件并执行了aboutToAppear()。原因如下:

  1. 首次渲染时,默认键值生成规则生成的键值依次为0__one、1__two、2__three;
  2. 插入新项后数据源变为['one', 'new item', 'two', 'three'],@State 监听到长度变化触发 ForEach 重新渲染;
  3. ForEach 依次遍历:
    • one生成键值0__one,已存在 → 不创建新组件;
    • new item生成键值1__new item,不存在 → 创建新组件并渲染;
    • two生成键值2__two,不存在 → 创建新组件并渲染;
    • three生成键值3__three,不存在 → 创建新组件并渲染。

尽管此示例中界面渲染结果符合预期,但每次插入一条新数组项时,ForEach 都会为从该数组项起后面的所有数组项全部重新创建组件。当数据源数据量较大或组件结构复杂时,组件无法复用,将导致性能体验不佳。因此,除非必要,否则不推荐将第三个参数keyGenerator处于缺省状态,也不推荐在键值生成规则中包含索引 index。

正确且高效的 ForEach 写法是为每个数据项生成稳定唯一的键值,例如:

ForEach(this.simpleList, itemGenerator: {item: String ,idx:Int64 =>ChildItem(item: item)}, keyGenerator: {item: String, index: Int64 => item})

提供了第三个参数keyGenerator后,在本例中它对数据源的不同数据项生成不同的 key,并且对同一个数据项每次生成相同的 key——这正是 ForEach 高效复用的底层保证:键值稳定 → 组件复用 → 渲染成本最小化。

小结

ForEach 循环渲染的正确性与性能,全部系于键值设计这一件事:

场景数据源类型推荐键值理由
数据源不变(如骨架屏)基本数据类型数组可缺省或使用数组项本身无增删,键值始终稳定
数据源数组项变化(插入/删除/交换)对象数组对象唯一 id键值不随索引漂移,最大程度复用组件
需条件渲染、必须用 index任意接受重建代价index 是保证键值唯一性的最后手段

核心结论可归纳为三条:其一,键值必须唯一且持久,重复键值会导致框架行为未定义;其二,键值生成规则中混入 index 会造成渲染结果非预期,缺省 keyGenerator 会造成插入点之后全量重建的性能损耗;其三,ForEach 适用于中等规模的全量渲染场景,大数据量场景应改用 LazyForEach,且二者不可在同一容器内混用。掌握键值规则,即可在仓颉 + ArkUI 应用中写出既正确又高效的循环渲染代码。

  • 文档
  • 知识库
  • 教程
  • OpenHarmony

【免费下载链接】CangjieCorpus

本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!

项目地址:https://gitcode.com/Cangjie/CangjieCorpus
点击查看免费下载

相关推荐

上一篇:imgaug KeepSizeByResize 单张图片数组输入崩溃问题(Issue 590)详解
下一篇:Cloudflare Skills零信任实战:Cloudflare One(SASE)的设计、配置与从Zscaler迁移

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 6:24:01

VR/360度视频传输优化(进阶篇)——工程师必备知识

本文深入探讨VR/360度视频传输优化&#xff08;进阶篇&#xff09;&#xff0c;涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。最近在处理视频与直播相关项目时&#xff0c;VR/360度视频传输优化&#xff08;进阶篇&#xff09;成为团队讨论最多的话题。经…

作者头像 李华
网站建设 2026/10/16 6:24:01

教育直播稳定性保障(进阶篇):原理、方案与优化

本文深入探讨教育直播稳定性保障&#xff08;进阶篇&#xff09;&#xff0c;涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。在视频与直播领域&#xff0c;教育直播稳定性保障&#xff08;进阶篇&#xff09;是开发者和技术负责人持续关注的核心议题。本文…

作者头像 李华
网站建设 2026/10/16 6:21:48

2021人工智能考研全流程指南:初试、复试与调剂策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:20:59

iTop PHPUnit 单元测试指南:从环境搭建到进程隔离的完整实践

后端企业应用 【免费下载链接】iTop A simple, web based CMDB & IT Service Management tool 项目地址&#xff1a; https://gitcode.com/gh_mirrors/it/iTop 点击查看 免费下载 本指南面向 iTop 开发者&#xff0c;系统讲解如何为 iTop 创建、维护 PHPUnit 单元测试与集…

作者头像 李华