首页 > 其他分享 >LazyForEach:数据渲染详解

LazyForEach:数据渲染详解

时间:2025-01-02 21:29:35浏览次数:3  
标签:index 渲染 list number LazyForEach item 详解 组件

一、LazyForEach 初印象

在当今的移动应用与 Web 开发领域,数据处理效率和性能优化是至关重要的话题。当面对大量数据需要展示时,如何既能保证流畅的用户体验,又能避免资源的过度消耗呢?这时候,LazyForEach 就如同一位 “智能管家” 闪亮登场。

LazyForEach 是一种用于实现数据懒加载的强大工具,它可不是一次性把所有数据和组件都一股脑儿地呈现出来,而是采取 “按需索取” 的策略。简单来说,只有当某个数据项进入用户的可视范围,也就是用户即将看到它的时候,才会触发该数据项对应组件的创建与加载。就好比你走进一个巨大的图书馆,不需要一次性把所有书架上的书都搬到眼前,而是走到哪个书架前,才去查看对应的书籍,节省了大量搬书的体力(类比系统资源)。这种特性使得它在处理长列表、网格布局、瀑布流等数据密集型场景时表现卓越,能够显著减少初始加载时间,降低内存占用,让应用运行得更加流畅,为用户带来丝滑般的体验。接下来,咱们就深入探究一下它的具体使用细节。

 二、LazyForEach 是什么

LazyForEach 是一种数据渲染策略,它允许开发者按需迭代数据源中的数据,并在每次迭代过程中创建相应的组件。这种方法通常用于滚动容器中,以便只渲染用户当前可见的数据项,从而优化性能和内存使用。当用户滚动时,LazyForEach 会动态加载和卸载数据项,以提供流畅的用户体验并减少不必要的计算。

与传统的 forEach 循环相比,LazyForEach 有着显著的区别。传统的 forEach 会一次性遍历整个数据源,为每一个数据项创建组件,无论这些组件是否会立即被用户看到。这就好比你准备一场盛宴,一次性把所有菜品都摆上桌,不管客人当下是否会品尝,导致桌子拥挤不堪(内存占用大),而且前期准备耗时久(加载时间长)。而 LazyForEach 则像是一位贴心的服务员,根据客人的需求,逐道上菜,客人看到哪道菜,才准备哪道菜对应的餐具(创建组件),大大节省了桌面空间(内存),也让客人能更快就座开席(减少初始加载时间)。在处理大规模数据集合时,这种差异带来的性能优势就愈发明显,能让应用在面对海量信息时依然保持轻盈、敏捷的姿态。

三、LazyForEach使用场景 

LazyForEach 的应用场景极为广泛,特别是在那些需要展示大量数据的界面中,它就像是一把性能优化的 “利器”。

在电商应用里,商品列表页面常常需要呈现海量的商品信息。想象一下,当用户打开一个大型购物平台,搜索某类商品时,可能会有成千上万条结果。若是采用传统的加载方式,一次性把所有商品的图片、标题、价格等信息对应的组件全部创建,不仅加载过程漫长,让用户对着空白屏幕干瞪眼,而且手机内存也会不堪重负,可能导致应用卡顿甚至闪退。而使用 LazyForEach,只有当用户滚动屏幕,商品即将进入可视区域时,才会迅速加载该商品的组件,让用户能够流畅地浏览商品,快速找到心仪之物,购物体验直线飙升。

社交媒体平台也是 LazyForEach 的 “主战场”。如今,人们每天都会在社交媒体上浏览大量的动态、帖子、图片和视频等信息。以朋友圈为例,当好友众多,动态不断更新时,整个动态列表的数据量十分惊人。LazyForEach 可以确保只有出现在屏幕上的动态才会被渲染,当用户快速滑动屏幕浏览时,新的动态平滑地加载出来,既节省了流量,又使得操作无比顺畅,让用户能轻松跟上朋友们的生活节奏。

新闻资讯类应用更是离不开 LazyForEach。在信息爆炸的时代,新闻资讯如潮水般涌来,一个热门话题下可能有数百条相关报道。用户在浏览新闻列表时,若一次性加载所有新闻内容,等待时间过长会让用户失去耐心。借助 LazyForEach,新闻标题、摘要和配图等组件按需加载,用户下滑屏幕时,新的新闻条目依次呈现,使得阅读新闻变得高效快捷,让用户能第一时间掌握天下大事。

除了上述场景,像音乐播放列表、视频播放历史、在线文档的章节列表、云盘文件列表等,只要涉及大量数据展示,LazyForEach 都能大显身手,优化性能,提升用户满意度,成为打造流畅、高效应用体验的得力助手。

 四、LazyForEach核心使用要点

(一)容器组件适配

LazyForEach 并非能在任意容器组件中 “肆意驰骋”,它有自己的 “专属座驾”。目前,它必须在特定的容器组件内使用,像是 List、Grid、Swiper 以及 WaterFlow 等组件,才是它的 “最佳搭档”。这些组件之所以能与 LazyForEach 完美配合,是因为它们都支持配置 cachedCount 属性。cachedCount 就像是一个智能的 “缓冲仓库”,它能够实现按需加载,只加载可视部分以及其前后少量数据用于缓冲,确保用户在滚动浏览时,数据能够快速、流畅地呈现。举个例子,当你在电商应用中浏览商品列表,滑动屏幕时,商品图片、标题、价格等组件能迅速映入眼帘,这背后就有 cachedCount 在默默助力,提前准备好周边即将出现的商品信息,避免等待加载的尴尬。倘若将 LazyForEach 放置在不支持的容器组件中,那它可就 “英雄无用武之地” 了,组件会一次性加载所有数据,之前精心设计的懒加载优化效果将化为泡影,应用性能也会大打折扣。

(二)组件数量规则

在 LazyForEach 的迭代过程中,有一条铁律:每次迭代必须且只能创建一个子组件。这意味着在 itemGenerator 函数(用于生成子组件的函数)中,不能出现同时创建多个子组件的情况。比如说,你不能在遍历数据源时,企图一次性为一个数据项创建一个文本组件、一个图片组件和一个按钮组件。这就好比一个流水线上的工人(LazyForEach),每次只能专注处理一个任务(创建一个子组件),将其打磨精细后再送往下一环节。如果违反这条规则,组件的渲染将会陷入混乱,可能出现组件重叠、样式错乱或者部分组件无法显示等问题,让界面变得一团糟,用户体验也会随之跌入谷底。

(三)条件渲染规则

LazyForEach 在条件渲染方面具有不错的灵活性,它允许包含在 if/else 条件渲染语句中,开发者可以依据不同的条件来决定是否渲染某个组件。不仅如此,在 LazyForEach 内部也可以出现 if/else 条件渲染语句,实现更精细的组件控制。想象一下,在一个社交动态列表中,对于付费会员用户,当他们浏览动态时,可以显示额外的专属标识、高级功能按钮等组件;而非会员用户看到的则是普通样式的动态。代码示例如下:

LazyForEach(this.dataSource, (item: any, index: number) => {
    if (isMember) {
        ListItem() {
            Row() {
                Text(item.content).fontSize(30)
                Image($r('app.media.member_icon')).width(30).height(30)
                Button('专属功能').onClick(() => { /* 专属功能逻辑 */ })
            }
        }
    } else {
        ListItem() {
            Row() {
                Text(item.content).fontSize(30)
            }
        }
    }
}, (item: any) => item.id)

在上述代码中,根据 isMember 这个状态变量,LazyForEach 能够智能地为不同用户渲染出差异化的组件,让应用既能满足个性化需求,又能在性能优化上不打折扣。

(四)键值生成要点

键值(Key)在 LazyForEach 的世界里扮演着至关重要的角色,它就像是每个组件的 “身份证”,必须具有唯一性。键值生成器的任务就是为每个数据生成独一无二的标识,若不同数据项生成了相同的键值,那将会引发 UI 组件渲染的 “灾难”,导致组件状态混乱、更新异常等问题。ArkUI 框架为开发者提供了一定的便利,如果开发者没有定义 keyGenerator 函数,框架会启用默认函数 (item: any, index: number) => { return viewId + '-' + index.toString(); }(其中 viewId 在编译器转换过程中生成,同一个 LazyForEach 组件内其 viewId 一致)。当然,为了满足更复杂、个性化的需求,开发者可以通过提供自定义的 keyGenerator 函数来掌控键值生成逻辑。比如,在一个新闻资讯列表中,若每条新闻都有唯一的 ID,就可以将这个 ID 作为键值,确保组件精准渲染与更新,代码如下:

LazyForEach(this.newsDataSource, (item: NewsItem, index: number) => {
    ListItem() {
        Text(item.title).fontSize(30)
        Text(item.summary).fontSize(20)
    }
}, (item: NewsItem) => item.newsId, (item: NewsItem, index: number) => item.newsId)

五、 LazyForEach实战演练

(一)基础搭建

首先,我们需要创建一个数据源,数据源要实现 IDataSource 接口,这个接口要求我们必须定义获取数据总数、根据索引获取数据、注册和注销数据改变监听器等方法。以下是一个简单的数据源示例代码:

class ListDataSource<T> implements IDataSource {
  /** 原始数组 */
  private originData: T[] = []
  /** 监听器 */
  private listener: DataChangeListener[] = [];

  /** 获取总数 */
  totalCount(): number {
    return this.originData.length;
  }

  /** 根据索引获取数据 */
  getData(index: number): T {
    return this.originData[index];
  }

  /** 添加一条数据 */
  addData(item: T) {
    this.originData.push(item);
  }

  /** 添加多条数据 */
  addDataList(items: T[]) {
    this.originData.push(...items);
    //通过listener通知数据更新
    this.listener.forEach(listener => {
      listener.onDataAdd(this.totalCount())
    })
  }

  /** 根据索引移除一条数据 */
  removeData(index: number) {
    this.originData.splice(index, 1);
  }

  /** 加载数据 */
  loadData(item: T[]) {
    this.originData = item;
  }

  /** 注册时触发 /注册监听 */
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listener.indexOf(listener) < 0) {
      this.listener.push(listener);
    }
  }

  /** 移除时触发 /解除监听 */
  unregisterDataChangeListener(listener: DataChangeListener): void {
    let index = this.listener.indexOf(listener)
    if (index > -1) {
      this.listener.splice(index, 1)
    }
  }
}

在上述代码中,BasicDataSource 类维护了一个数据数组 originData,并实现了 IDataSource 接口的各个方法,用于管理数据的增删改查以及监听数据变化。

接着,在组件中引入 LazyForEach,假设我们要展示一个简单的文本列表,代码如下:

@Entry
@Component
struct LazyForEachCase {
  list: ListDataSource<number> = new ListDataSource<number>();
  loading: boolean = false

  aboutToAppear(): void {
    this.list.addDataList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  }

  appendData() {
    let newArr = Array.from(Array(10), (item: number, index: number) => {
      return index + 1 + this.list.totalCount()
    })
    this.list.addDataList(newArr)
  }

  build() {
    List({ space: 20 }) {
      LazyForEach(this.list, (item: number, index: number) => {
        ListItem() {
          Text(`第${index + 1}个`)
            .fontSize(30)
            .fontColor(Color.White)
        }
        .height(60)
        .width("100%")
        .backgroundColor(Color.Pink)
        .borderRadius(10)
        .onAppear(() => {
          console.log(`第${index}个创建`)
        })
        .onDisAppear(() => {
          console.log(`第${index}个销毁`)
        })
      })
    }
    .padding(20)
  }
}

在这个例子中,LazyForEachCase 组件初始化了一个 ListDataSource 类型的数据源 list,并在 aboutToAppear 生命周期钩子函数中添加了初始数据。在 build 函数里,通过 LazyForEach 遍历数据源,为每个数据项创建一个粉色背景、带有白色文本的 ListItem 组件,文本显示当前项的索引。当列表项进入可视区域时,会在控制台打印 “第 x 个创建”,离开可视区域时打印 “第 x 个销毁”,这样就能清晰看到懒加载的效果。

(二)数据更新操作

当需要动态更新数据时,比如添加新数据,我们可以在数据源类中定义相应的方法,并在组件中触发。为列表添加一个事件监听器,当列表滚动到底部时触发。

onReachEnd

onReachEnd(event: () => void)

列表到底末尾位置时触发。

List边缘效果为弹簧效果时,划动经过末尾位置时触发一次,回弹回末尾位置时再触发一次。

卡片能力: 从API version 9开始,该接口支持在ArkTS卡片中使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.ArkUI.ArkUI.Full

@Entry
@Component
struct LazyForEachCase {
  list: ListDataSource<number> = new ListDataSource<number>();
  loading: boolean = false

  aboutToAppear(): void {
    this.list.addDataList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  }

  appendData() {
    let newArr = Array.from(Array(10), (item: number, index: number) => {
      return index + 1 + this.list.totalCount()
    })
    this.list.addDataList(newArr)
  }

  build() {
    List({ space: 20 }) {
      LazyForEach(this.list, (item: number, index: number) => {
        ListItem() {
          Text(`第${index + 1}个`)
            .fontSize(30)
            .fontColor(Color.White)
        }
        .height(60)
        .width("100%")
        .backgroundColor(Color.Pink)
        .borderRadius(10)
        .onAppear(() => {
          console.log(`第${index}个创建`)
        })
        .onDisAppear(() => {
          console.log(`第${index}个销毁`)
        })
      })
    }
    .padding(20)
    .onReachEnd(() => {
      if (!this.loading) {
        this.loading = true
        this.appendData()
        this.loading = false
      }
    })
  }

这里定义了 appendData 方法,点击按钮时调用该方法,它会生成一个新的数组,数组元素是基于当前数据总数递增的序号,然后将新数组添加到数据源中,LazyForEach 会自动检测到数据变化,按需创建新的组件并渲染。

删除数据的操作类似,在数据源类中添加 removeData 方法,在组件中为列表项添加点击删除的逻辑:

@Entry
@Component
struct LazyForEachCase {
    list: ListDataSource<number> = new ListDataSource<number>();
    aboutToAppear(): void {
        // 在组件即将显示时调用,向列表中添加初始数据
        this.list.addDataList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    }
    removeItem(index: number) {
        this.list.removeData(index);
    }
    build() {
        List({ space: 20 }) {
            LazyForEach(this.list, (item: number, index: number) => {
                ListItem() {
                    Text(`第${index + 1}个`)
                       .fontSize(30)
                       .fontColor(Color.White)
                }
               .height(60)
               .width("100%")
               .backgroundColor(Color.Pink)
               .borderRadius(10)
               .onAppear(() => {
                    console.log(`第${index + 1}个创建`)
                })
               .onDisAppear(() => {
                    console.log(`第${index + 1}个销毁`)
                })
               .onClick(() => {
                    this.removeItem(index);
                })
            })
        }
       .padding(20)
    }
}

点击列表项时,会调用 removeItem 方法,根据索引从数据源中移除对应的数据项,LazyForEach 也会相应地销毁对应的组件。

交换数据的操作稍微复杂一些,需要记录两个要交换的数据项索引,然后在数据源类中实现交换逻辑:

@Entry
@Component
struct LazyForEachCase {
    list: ListDataSource<number> = new ListDataSource<number>();
    aboutToAppear(): void {
        // 在组件即将显示时调用,向列表中添加初始数据
        this.list.addDataList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    }
    moveData(fromIndex: number, toIndex: number) {
        let fromItem = this.list.getData(fromIndex);
        let toItem = this.list.getData(toIndex);
        this.list.removeData(fromIndex);
        this.list.removeData(toIndex);
        this.list.addData(toIndex, fromItem);
        this.list.addData(fromIndex, toItem);
    }
    build() {
        List({ space: 20 }) {
            LazyForEach(this.list, (item: number, index: number) => {
                ListItem() {
                    Text(`第${index + 1}个`)
                       .fontSize(30)
                       .fontColor(Color.White)
                }
               .height(60)
               .width("100%")
               .backgroundColor(Color.Pink)
               .borderRadius(10)
               .onAppear(() => {
                    console.log(`第${index + 1}个创建`)
                })
               .onDisAppear(() => {
                    console.log(`第${index + 1}个销毁`)
                })
            })
        }
       .padding(20)
        Button('交换数据').onClick(() => {
            // 假设交换前两个数据项,实际应用中可按需获取索引
            this.moveData(0, 1); 
        })
    }
}

在上述代码中,点击 “交换数据” 按钮,会调用 moveData 方法,先获取要交换的两个数据项,从数据源中移除它们,再按照交换后的索引重新添加,从而实现数据项位置的交换,LazyForEach 会智能地更新组件显示,确保界面正确反映数据变化。通过这些操作示例,开发者可以根据实际需求灵活运用 LazyForEach 构建出动态、高效的数据展示界面。

六、LazyForEach常见问题解答

(一)组件未更新

当数据源中的数据发生变化,但组件却没有如预期般更新时,这可能是由于键值(Key)的问题。如果键值生成器为不同的数据生成了相同的键值,LazyForEach 会误认为组件没有变化,从而不会触发更新。例如,在一个待办事项列表中,若以事项的内容作为键值,当事项内容被编辑后,新的内容与原内容相同,就会出现键值重复,导致组件不更新。解决办法是确保键值具有唯一性,比如结合事项的 ID 和编辑时间戳等信息来生成键值,让 LazyForEach 能精准识别数据变化,及时更新组件。

(二)渲染异常

有时组件可能会出现渲染异常,如部分组件丢失、重叠或者样式错乱。这大概率是因为违反了 LazyForEach 的组件数量规则,即在 itemGenerator 函数中创建了多个子组件。例如,在构建一个商品列表时,不小心在遍历数据源时,为每个商品数据同时创建了图片组件、标题组件和价格组件,还额外添加了一个促销标签组件,就会导致渲染混乱。此时,需要仔细检查代码,确保每次迭代只创建一个符合规范的子组件,将多余的组件创建逻辑拆分到合适的位置,遵循 LazyForEach 的规则,使渲染恢复正常。

(三)性能不佳

即使使用了 LazyForEach,应用性能仍不理想,可能是没有合理配置 cachedCount 属性。若 cachedCount 设置过小,当用户快速滑动屏幕时,LazyForEach 来不及加载新的数据和组件,会出现短暂的空白区域,影响用户体验;反之,若设置过大,又会占用过多内存,失去懒加载节省内存的优势。比如在新闻资讯列表中,若 cachedCount 设为 1,快速下滑时新新闻加载缓慢,设为 100,虽然滑动流畅但内存消耗剧增。开发者需要根据实际情况,如数据加载速度、设备性能等,反复测试调整 cachedCount 的值,找到性能与资源占用的平衡点,让 LazyForEach 发挥最佳效能。

 七、总结

通过本文的详细介绍,相信大家对 LazyForEach 已经有了较为深入的理解。它的核心要点包括适配特定容器组件、遵循组件数量规则、巧用条件渲染以及确保键值唯一性等,这些要点如同基石,支撑着高效的数据展示与交互体验。在实战演练中,无论是基础的搭建,还是复杂的数据更新操作,LazyForEach 都展现出强大的功能,帮助我们构建流畅、响应迅速的应用界面。同时,针对常见问题的剖析,让我们能提前避开陷阱,保障应用的稳定运行。

掌握 LazyForEach 对于开发者而言,意味着在面对海量数据时能够游刃有余,提升应用性能,降低资源消耗,进而增强用户满意度与忠诚度。展望未来,随着移动应用和 Web 开发的持续发展,数据量将愈发庞大,用户对体验的要求也会越来越高,LazyForEach 必将在更多场景中大放异彩。希望各位开发者能将所学运用到实际项目中,不断探索创新,挖掘 LazyForEach 的更多潜力,为用户打造更加出色的产品体验。

标签:index,渲染,list,number,LazyForEach,item,详解,组件
From: https://blog.csdn.net/hqy1989/article/details/144850331

相关文章

  • Wireshark中的名称解析设置详解
    在网络流量分析中,数据包中常常包含各种地址(如MAC地址、IP地址)或协议名称,这些信息以数值形式显示会让分析变得困难。Wireshark提供了名称解析(NameResolution)功能,可以将这些信息解析成更易于理解的名称,从而提升数据分析的效率和准确性。本文将详细介绍名称解析设置界面的各......
  • AIGC 爆款工具 Stable Diffusion 教程详解,带你解锁绘画新境界
    前言StableDiffusion乃是一款依托人工智能技术打造的绘画软件。该软件运用生成对抗网络(GAN)这一深度学习模型,通过学习并模仿艺术家的创作风格,进而生成与之类似的艺术作品。以下将为你带来StableDiffusion的教程详解,涵盖软件介绍、配置要求、安装步骤以及基础操作等多方......
  • 详解:促销系统整体规划
    大家好,我是汤师爷~今天聊聊促销系统整体规划。各类促销活动的系统流程,可以抽象为3大阶段:B端促销活动管理:商家运营人员在后台系统中配置和管理促销活动,包括设定活动基本信息、使用规则、选择适用商品等核心功能。C端促销活动参与:消费者在前台系统中浏览和参与促销活动,并在下单......
  • MyBatis 结果映射详解:resultType 与 resultMap
    MyBatis结果映射详解:resultType与resultMap在MyBatis中,结果映射是将数据库查询结果集(ResultSet)映射到Java对象的关键步骤。MyBatis提供了两种主要的方式来处理结果映射:resultType和resultMap。本文将详细介绍这两种方式的使用场景、配置方法以及最佳实践,帮助开发者更好......
  • 高并发解决方案详解
    微服务拆分分布式架构会从一个拆分为多个系统,每个系统都有独立的数据库等,通过这样的横向扩展,就可以支撑更大的并发量。异步处理对于一些耗时的操作,比如:下订单后的发短信,并发量大的情况下同步操作极为耗时,需要改造为异步请求。如下图所示: 负载均衡负载均衡(LoadBalancing......
  • 详解 使用结构体内存布局直接映射
    使用结构体内存布局直接映射数据帧详解在某些固定格式的数据帧解析中,直接将二进制数据帧映射到结构体是一个高效且简洁的方式。这种方法利用结构体的内存布局直接解析数据帧的字段,而无需手动逐字节处理。以下是更详细的分步骤说明及示例:核心思路结构体布局直接映射:将固......
  • 详解AQS五:深入理解共享锁CountDownLatch
    CountDownLatch是一个常用的共享锁,其功能相当于一个多线程环境下的倒数门闩。CountDownLatch可以指定一个计数值,在并发环境下由线程进行减一操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒。通过CountDownLatch可以实现线程间的计数同步。为什么说CountDownLatch是一种共享......
  • [Qt] 万字详解Qt入门~ Qt Creator | 对象树 | 控件布局
    目录1.QtCreator概览2.使用QtCreator新建项目3.认识QtCreator界面4.QtHelloWorld程序1.使用“按钮”实现2.使用“标签”实现3.使用“编辑框”实现5.项目文件解析1.命名空间声明与作用2.classWidget:publicQWidget6.Qt编程注意事项......
  • 使用JSONObject.getString()时报错:Cannot resolve method ‘getString‘ in ‘JSONObjec
    目录使用JSONObject.getString()时报错:Cannotresolvemethod'getString'in'JSONObject',JSONObject三种库的用法一、背景描述二、问题解决1、使用org.json.JSONObject读取属性2、使用org.json.simple.JSONObject读取属性3、使用cn.hutool.json.JSONObject读取属性三、......
  • 【信息系统项目管理师】第14章:项目沟通管理过程详解
    更多内容请见:备考信息系统项目管理师-专栏介绍和目录文章目录一、规划沟通管理1、输入2、工具与技术3、输出二、管理沟通1、输入2、工具与技术3、输出三、监督沟通1、输入2、工具与技术3、输出一、规划沟通管理定义:规划沟通管理是......