文档开发者中心
我的
|

ArkTS

LazyForEach 加载大量数据

LazyForEach 加载大量数据,列表滑动到底触发加载更多,数据追加成功,但滚动位置莫名跳回顶部,只在分包页面出现这个现象。
点赞
收藏
回复
10
分享
举报
浏览153 发布于4天前广西
全部评论
最多点赞
最新发布
最早发布

先看分包页是不是追加后重新创建了 DataSource、Scroller 或整个 List。分页只改同一个源的尾部,并按追加索引发通知,旧行继续用原有业务 ID。

收起
自动换行
深色代码主题
复制
append(row: RowData): void {
  const index = this.rows.length;
  this.rows.push(row);
  this.listeners.forEach(listener => listener.onDataAdd(index));
}

不要把每次分页都变成全量重载。若还是回顶,记录 List 和分包页的创建次数;只有分包重新出现时复现,再查路由是否重建页面。

8楼回复于4天前 来自四川
根源在于数据追加时触发了全量重载,导致 LazyForEach 组件树被整体重建。分包页面的加载时序不稳定,会放大这一现象,让它比在主包页面中更明显。而LazyForEach 加载大量数据,列表滑动到底触发加载更多,数据追加成功,但滚动位置莫名跳回顶部,只在分包页面出现这个现象。

错误的方式:全量重载:调用 onDataReloaded() 或直接对数据源整体重新赋值(如 this.dataSource = new MyDataSource(newList)),会通知框架“所有数据已变”,导致所有已渲染节点被销毁并重建。List 无法依赖上一帧的布局数据,滚动位置自然丢失。

1. 使用精确的增量通知在数据源中,追加数据时必须调用 onDataAdd,而非 onDataReloaded:```typescript// 正确写法:尾部批量追加pushMessages(batch: Message[]) { const start = this.messages.length this.messages.push(...batch) // 关键:精确通知框架从 start 开始新增了 batch.length 条 this.listeners.forEach((l) => l.onDataAdd(start, batch.length))}```

2. 数据源避免被 @State 修饰LazyForEach 的数据源本身不应被 @State 或 @Local 修饰。数据的变化应完全通过 notifyXxx 系列方法(onDataAdd/onDataChange 等)通知框架,而不是通过替换整个数据源引用来触发 UI 更新。

3. 确保 keyGenerator 返回稳定唯一的 ID```typescript// 正确:使用业务唯一IDLazyForEach(this.dataSource, (item: Message) => { ListItem() { ... }}, (item: Message) => item.id) // 关键:返回稳定ID。
1
7楼编辑于4天前 来自山东
  • 匿名用户匿名用户
    很不错的回答
    4天前 来自北京

这个问题大多是两个原因,一是LazyForEach用索引当key,新增数据后key全部变动,框架会重建列表,二是数据源没有走增量通知,直接整体刷新。分包页面的组件缓存机制和主包不一样,同样的写法更容易出现滚动位置重置跳回顶部。

优先改这两点,第一keyGenerator换成业务唯一id,不要用index。第二自己实现IDataSource,追加数据的时候调用onDataAdd,不要用notifyDataReloaded全量刷新。

额外注意几个分包页面容易踩的坑,不要在aboutToAppear里重复新建数据源实例,List组件外面不要用if控制显隐,避免列表组件被销毁重建。上拉加载加上加载锁,防止onReachEnd短时间重复请求。

如果改完还是不稳定,可以搭配scroller监听滚动,记录当前可见索引,数据追加完成后延迟恢复滚动位置。新版本也可以考虑换成Repeat组件,key机制更稳定。

6楼回复于4天前 来自江西
LazyForEach 追加跳顶源于全量重载致组件重建,分包时序放大问题。应改用 onDataAdd 精确通知、数据源不加状态装饰器、key 用业务唯一 id。
5楼回复于4天前 来自江苏

结论:2 楼的排查框架(数据源被整体刷新 / 组件重建 / Key 不稳定 / 分包生命周期)方向是对的,但有一处版本错误需要纠正,另外官方文档对 maintainVisibleContentPosition 的两条行为边界原文,恰好能把「底部追加却跳回顶部」这个现象解释得更死。

一、纠错:maintainVisibleContentPosition 从 API 12 就支持,不是「从 API 20 开始支持相关能力」
官方 List 属性表中该属性的签名直接标注为 maintainVisibleContentPosition12+,功能描述原句:「设置显示区域上方插入或删除数据时是否要保持可见内容位置不变」。
API 20 的更新点是另一件事,原句:「从API version 20开始,使用Repeat在懒加载场景下,显示区域外插入或删除数据时,属性设置为true也能保持可见内容位置不变」——即 20 起把 Repeat 懒加载场景纳入保护,而不是这个属性从 20 才开始存在。2 楼写「它从 API 20 开始支持相关能力」会把低版本项目误导向「这个属性用不了」的结论。

二、官方行为边界:这个属性保护不了你的场景
两条官方原句:
①「只有使用LazyForEach在显示区域外插入或删除数据时,属性设置为true才能保持可见内容位置不变。使用ForEach插入或删除数据、使用LazyForEach重新加载数据时,即使maintainVisibleContentPosition属性设置为true,可见区内容位置也会跟随变化。」
②该属性的功能定义只针对「显示区域上方插入或删除数据」。
对应到你的现象:
· 你是底部追加(滑动到底触发加载更多),不是「显示区域上方」插入——这个属性本来就不覆盖底部追加场景;
· 只要加载更多的路径上出现了 notifyDataReload(或数据源被整体替换/重建),「可见区内容位置也会跟随变化」是官方写明的行为,跳回顶部不是偶发 bug,而是 reload 的确定性后果。

三、对「只有分包页面出现」的解释建议
结合上面两条边界,2 楼列的原因里最应优先验证的是「加载更多时执行了 notifyDataReload」和「分包页 aboutToAppear 重复执行导致数据源重建」这两个,因为它们触发的跳顶是官方行为;而 Key 不稳定、onReachEnd 重复触发等属于实现层问题,任何页面都会踩,不构成「仅分包出现」的解释。分包页排查时把「加载更多回调里实际调用了 DataSource 的哪个通知方法」逐行记下来,对比主包同名页面,通常一轮就能定位到差异点。

说明:以上依据官方文档整理,未在实际工程中编译运行,不声称「已实测」。依据(链接已实际打开核对原文):
List 组件参考(maintainVisibleContentPosition12+ 签名、功能描述原句、LazyForEach/ForEach/Reload 行为边界原句、API 20 Repeat 扩展原句):https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-container-list

展开全部
4楼回复于4天前 来自湖北

这个问题大概率是两个坑叠在一起了:数据追加时用了错误的刷新通知方式,加上分包页面组件生命周期导致数据源被重建。

根因分析

坑一:用了 onDataReloaded 通知追加数据

LazyForEach 的 DataChangeListener 提供了好几种通知方法,作用完全不同:

  • onDataReloaded() — 通知框架重新加载全部数据,等同于"数据全换了",框架会重建整个列表,滚动位置自然归零
  • onDataAdd(index) / onDatasetChange([DataAddOperation]) — 通知框架在指定位置追加了数据,框架只增量创建新组件,不影响已有组件和滚动位置

如果你的加载更多逻辑里,追加完数据后调的是 notifyDataReloaded()(或者直接对整个 dataArray 重新赋值再 reload),滚动位置必然跳回顶部。官方文档明确说了"重新赋值第一个参数 dataSource 会导致异常"。

坑二:为什么只在分包页面出现

HSP 分包页面的组件生命周期和主包页面有差异。分包页面在 Navigation 栈中进退时,组件更容易被销毁和重建。如果你的 DataSource 是组件的局部变量(private data: MyDataSource = new MyDataSource()),组件重建时 DataSource 也跟着重建,所有数据重新加载 → 滚动位置归零。

主包页面通常不会频繁销毁重建,所以同样的代码在主包页面可能表现正常。

解决方案

第一步:追加数据用 notifyDataAdd,不要用 notifyDataReloaded

收起
自动换行
深色代码主题
复制
class PageDataSource implements IDataSource {
  private dataArray: string[] = [];
  private listeners: DataChangeListener[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): string {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  pushData(data: string): void {
    this.dataArray.push(data);
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataAdd(this.dataArray.length - 1);
    });
  }

  appendList(datas: string[]): void {
    const startIndex = this.dataArray.length;
    this.dataArray.push(...datas);
    this.listeners.forEach((listener: DataChangeListener) => {
      const addOp: DataAddOperation = {
        type: DataOperationType.ADD,
        index: startIndex,
        count: datas.length
      };
      listener.onDatasetChange([addOp]);
    });
  }
}

关键点:追加数据时用 onDataAdd 或 onDatasetChange + DataAddOperation,只通知增量部分,框架不会重建已有组件,滚动位置不会变。

第二步:分包页面 DataSource 用单例或 AppStorageV2 持久化

避免 DataSource 随组件重建而重建:

收起
自动换行
深色代码主题
复制
import { AppStorageV2 } from '@kit.ArkUI';

class DataSourceHolder {
  private static instance: PageDataSource | null = null;

  static getInstance(): PageDataSource {
    if (!DataSourceHolder.instance) {
      DataSourceHolder.instance = new PageDataSource();
    }
    return DataSourceHolder.instance;
  }
}

@Entry
@Component
struct SubPackageListPage {
  private dataSource: PageDataSource = DataSourceHolder.getInstance();
  private listScroller: Scroller = new Scroller();
  private savedFirstIndex: number = 0;

  aboutToAppear(): void {
    if (this.dataSource.totalCount() === 0) {
      for (let i = 0; i < 30; i++) {
        this.dataSource.pushData(`Item ${i}`);
      }
    }
  }

  build() {
    Column() {
      List({ space: 10, scroller: this.listScroller }) {
        LazyForEach(this.dataSource, (item: string) => {
          ListItem() {
            Text(item)
              .width('100%')
              .height(80)
              .textAlign(TextAlign.Center)
              .backgroundColor('#4169E1')
              .fontColor(Color.White)
              .borderRadius(12)
          }
        }, (item: string) => item)
      }
      .width('100%')
      .layoutWeight(1)
      .cachedCount(5)
      .onScrollIndex((start: number) => {
        this.savedFirstIndex = start;
      })
      .onReachEnd(() => {
        this.loadMore();
      })
    }
    .width('100%')
    .height('100%')
  }

  private loadMore(): void {
    const currentCount = this.dataSource.totalCount();
    for (let i = 0; i < 20; i++) {
      this.dataSource.pushData(`Item ${currentCount + i}`);
    }
  }
}

第三步(兜底):用 Scroller 记录并恢复滚动位置

如果前两步做完还有跳动,加一个 Scroller 兜底:

收起
自动换行
深色代码主题
复制
.onReachEnd(() => {
  const currentIndex = this.savedFirstIndex;
  this.loadMore();
  setTimeout(() => {
    this.listScroller.scrollToIndex(currentIndex);
  }, 50);
})

相关文档:List 的下拉加载如何回滚到当前展示位置

完整最小 Demo

下面是完整可运行的示例,对比了错误写法和正确写法:

收起
自动换行
深色代码主题
复制
import { hilog } from '@kit.PerformanceAnalysisKit';

class ItemData {
  id: string = '';
  title: string = '';

  constructor(id: string, title: string) {
    this.id = id;
    this.title = title;
  }
}

class LazyDataSource implements IDataSource {
  private dataArray: ItemData[] = [];
  private listeners: DataChangeListener[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): ItemData {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  pushData(data: ItemData): void {
    this.dataArray.push(data);
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataAdd(this.dataArray.length - 1);
    });
  }

  appendBatch(datas: ItemData[]): void {
    const startIndex = this.dataArray.length;
    this.dataArray.push(...datas);
    this.listeners.forEach((listener: DataChangeListener) => {
      const addOp: DataAddOperation = {
        type: DataOperationType.ADD,
        index: startIndex,
        count: datas.length
      };
      listener.onDatasetChange([addOp]);
    });
  }

  reloadAll(datas: ItemData[]): void {
    this.dataArray = datas;
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataReloaded();
    });
  }
}

class DataSourceManager {
  private static instance: LazyDataSource | null = null;

  static getInstance(): LazyDataSource {
    if (!DataSourceManager.instance) {
      DataSourceManager.instance = new LazyDataSource();
    }
    return DataSourceManager.instance;
  }

  static resetInstance(): void {
    DataSourceManager.instance = null;
  }
}

@Entry
@Component
struct LazyForEachScrollDemo {
  private dataSource: LazyDataSource = DataSourceManager.getInstance();
  private listScroller: Scroller = new Scroller();
  private savedFirstIndex: number = 0;
  @State loadCount: number = 0;

  aboutToAppear(): void {
    if (this.dataSource.totalCount() === 0) {
      this.doLoadMore();
    }
  }

  build() {
    Column() {
      Text('LazyForEach 加载更多 Demo')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 10 })

      Text(`已加载 ${this.loadCount} 批,共 ${this.dataSource.totalCount()} 条`)
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 10 })

      List({ space: 10, scroller: this.listScroller }) {
        LazyForEach(this.dataSource, (item: ItemData) => {
          ListItem() {
            Row() {
              Text(item.title)
                .fontSize(16)
                .fontColor(Color.White)
            }
            .width('100%')
            .height(80)
            .borderRadius(12)
            .backgroundColor('#4169E1')
            .justifyContent(FlexAlign.Center)
          }
        }, (item: ItemData) => item.id)
      }
      .width('100%')
      .layoutWeight(1)
      .cachedCount(5)
      .onScrollIndex((start: number) => {
        this.savedFirstIndex = start;
      })
      .onReachEnd(() => {
        this.doLoadMore();
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  private doLoadMore(): void {
    const currentCount = this.dataSource.totalCount();
    const newDatas: ItemData[] = [];
    for (let i = 0; i < 20; i++) {
      newDatas.push(new ItemData(`id-${currentCount + i}`, `Item ${currentCount + i}`));
    }
    this.dataSource.appendBatch(newDatas);
    this.loadCount++;
  }
}

这个 demo 里 appendBatch 用的是 onDatasetChange + DataAddOperation,追加数据不会重建已有组件,滚动位置保持不变。如果你把 appendBatch 换成 reloadAll(内部调 onDataReloaded),就能复现跳回顶部的问题。

排查优先级:先检查你加载更多时调的是哪种通知方法,这个是最常见的原因;然后再看分包页面 DataSource 是否被意外重建。

展开全部
3楼回复于4天前 来自四川

结论:LazyForEach 追加数据后列表跳回顶部,通常不是懒加载本身的问题,而是数据源被整体刷新、组件重新创建,或者 Scroller 状态被重置。

你提到的“只有分包页面出现”是关键线索,需要重点检查分包页面的生命周期和数据源初始化逻辑,不能直接认定是分包机制的 Bug。

一、优先排查这 5 个原因

可能原因

典型问题

数据源全量刷新

追加数据后调用了 notifyDataReload()

DataSource 被重新创建

每次加载更多都执行 new DataSource()

Key 不稳定

使用 index、随机数作为 Key

List 被重新构建

使用 if 控制 List,或父组件状态变化导致重建

分包页面生命周期异常

加载更多时再次执行 aboutToAppear() 并重置列表

其中,官方文档明确指出,LazyForEach 重新加载数据与单条数据插入的处理机制不同,全量重载可能影响可见内容位置。

二、正确的数据追加方式

关键:不要整体替换 DataSource,应在原数据源中追加数据,并逐项通知新增。

例如:

收起
自动换行
深色代码主题
复制
class ListDataSource implements IDataSource {
  private items: string[] = [];
  private listeners: DataChangeListener[] = [];

  totalCount(): number {
    return this.items.length;
  }

  getData(index: number): string {
    return this.items[index];
  }

  registerDataChangeListener(
    listener: DataChangeListener
  ): void {
    this.listeners.push(listener);
  }

  unregisterDataChangeListener(
    listener: DataChangeListener
  ): void {
    this.listeners = this.listeners.filter(
      (item: DataChangeListener) => item !== listener
    );
  }

  appendData(newItems: string[]): void {
    for (let item of newItems) {
      const index = this.items.length;
      this.items.push(item);

      this.listeners.forEach(
        (listener: DataChangeListener) => {
          listener.onDataAdd(index);
        }
      );
    }
  }
}

这里的字符串应使用稳定、唯一的业务 ID 或由 ID 唯一标识的数据项,不要让重复的文本内容充当 Key。

列表中保持 DataSource 和 Scroller 实例稳定:

收起
自动换行
深色代码主题
复制
@State loading: boolean = false;

private dataSource: ListDataSource = new ListDataSource();
private scroller: Scroller = new Scroller();

build() {
  List({ scroller: this.scroller }) {
    LazyForEach(this.dataSource,
      (item: string) => {
        ListItem() {
          Text(item)
        }
      },
      (item: string) => item
    )
  }
  .onReachEnd(() => {
    if (!this.loading) {
      this.loadMore();
    }
  })
}

loadMore() 中应先判断是否正在加载、是否还有下一页。onReachEnd 在边缘回弹等场景可能重复触发,因此不能只依赖它控制分页次数。

三、只有分包页面异常,应该重点检查什么?

建议在分包页面打印这些日志:

收起
自动换行
深色代码主题
复制
aboutToAppear(): void {
  console.info('SubPage aboutToAppear');
}

aboutToDisappear(): void {
  console.info('SubPage aboutToDisappear');
}

同时在以下位置记录时间:

  • loadMore() 开始、结束。

  • DataSource 创建和 onDataAdd() 调用。

  • List 的 onScrollIndex()。

  • List.onAppear() 和 onDisappear()。

根据现象判断:

情况 A:加载更多时,aboutToAppear() 再次执行

重点排查分包路由、Navigation 页面实例是否被重新创建,以及 aboutToAppear() 中是否有清空数据源、初始化 Scroller 的逻辑。

情况 B:页面没有重新创建,但滚动回顶部

优先排查 notifyDataReload()、Key、List 条件渲染及数据源整体替换。

情况 C:数据追加完成后先显示顶部,再恢复原位置

可能存在页面重建后的延迟滚动恢复,或业务代码主动调用了 scrollToIndex(0)、scrollEdge(Edge.Start)。

四、可以使用 maintainVisibleContentPosition 吗?

可以评估:

收起
自动换行
深色代码主题
复制
List({ scroller: this.scroller }) {
  LazyForEach(this.dataSource,
    (item: ItemData) => {
      ListItem() {
        // Item 内容
      }
    },
    (item: ItemData) => item.id.toString()
  )
}
.maintainVisibleContentPosition(true)

不过这个接口主要解决可视区域外插入、删除数据后的位置保持,不能解决 DataSource 整体重建或 List 被销毁的问题。它从 API 20 开始支持相关能力。

对于你这种“底部追加数据直接回顶部”的现象,优先修复数据更新和组件生命周期问题,不建议一开始就依赖这个属性。

五、推荐的排查顺序

  1. 检查加载更多时有没有调用 notifyDataReload()。

  2. 确保 DataSource、Scroller 不会因为父组件状态变化而重新创建。

  3. 检查 Key 是否使用稳定的业务 ID。

  4. 对比分包和主包页面的 aboutToAppear()、onAppear() 日志。

  5. 暂时去掉 @Reusable、复杂 Item 和额外滚动定位逻辑,使用简单文本 Item 复现。

  6. 如果只有分包稳定复现,且上述情况全部排除,再提交最小 Demo 给华为定位。

展开全部
2楼回复于4天前 来自山西
AI 生成内容,仅供参考

LazyForEach 追加数据后列表跳回顶部,本质上是"数据全量重载导致组件树整体重建"引起的,与分包本身没有直接因果关系,但分包页面的加载时序会放大这个问题。下面从原理、分包场景的排查点、正确写法三方面说明。

问题原理

LazyForEach 依赖数据源实现的 IDataSource 接口,通过 DataChangeListener 的回调向框架"报告"数据变化方式。框架提供了两类通知:

  • onDataReloaded():通知框架所有数据已变更,框架会销毁所有已渲染的节点并重新创建,List 布局时无法依赖上一帧的布局数据获取正常偏移量,表现为滚动位置丢失、跳顶或滚动条跳动。
  • onDataAdd(index) / onDataChange(index) / onDataDelete(index) 等精确通知:只在对应位置更新节点,不影响当前可见区域的布局,追加数据时滚动位置保持不变。

触底加载更多属于"尾部追加"场景,必须使用 onDataAdd 精确通知。如果当前代码在追加后调用了 notifyDataReload(),或者干脆对数据源整体重新赋值(例如 this.dataSource = new MyDataSource(newList)、把数据源数组整体替换),就会触发全量重建,滚动位置被重置。

为什么只在分包页面出现

分包页面是动态加载的,组件挂载与数据就绪的时序和主包页面不同,以下几种写法在分包页面更容易暴露问题:

  1. 数据源被 @State 或 @Local 修饰。状态装饰器对数据源做引用级观测,一旦在加载回调中重新赋值数据源引用,整个 LazyForEach 会重新挂载。分包页面首次进入时数据源通常在异步回调中才完成初始化,重建时列表尚未稳定,跳顶现象比主包更明显。数据源本身不应被状态装饰器修饰,数据变化应通过 notifyXxx 通知框架。
  2. 分包页面中数据请求回调里重新创建数据源实例,或先清空再填充(dataArray = [] 后再 push + reload)。这种"清空重灌"在主包页面若组件已渲染稳定,可能只表现为轻微闪动;分包页面首次渲染未完成时则直接回到顶部。
  3. keyGenerator 使用 index 作为键值。框架默认键值生成函数只受 index 影响,追加数据后若框架判定键值变化,会重建子组件。键值应使用业务侧唯一且固定的 id。

正确实现示例

以下代码演示标准的"触底加载更多"写法,适用于 API 12 及以上版本:

收起
自动换行
深色代码主题
复制
// BasicDataSource.ets
// 基础数据源,负责管理监听者和数据本身
import { IDataSource, DataChangeListener } from '@kit.ArkUI';

export class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private dataArray: T[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): T {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos: number = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  // 通知指定位置新增数据,不会影响可见区域布局
  notifyDataAdd(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataAdd(index);
    });
  }

  // 全量重载,触底加载场景禁止使用,会导致滚动位置重置
  notifyDataReload(): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataReloaded();
    });
  }
}

// MyDataSource.ets
// 业务数据源,追加数据使用精确通知
export class NewsDataSource extends BasicDataSource<NewsItem> {
  public pushDataList(items: NewsItem[]): void {
    const startIndex: number = this.totalCount();
    for (let i = 0; i < items.length; i++) {
      // 注意:此处需在 BasicDataSource 中暴露受保护的 dataArray 操作,
      // 或直接在子类中持有数组,核心是逐条 push 后按位置通知
      this.appendItem(items[i]);
    }
    void startIndex; // 起始索引可用于日志排查
  }

  private appendItem(item: NewsItem): void {
    // 向数组尾部追加,并通知框架该位置有新数据
    // (追加实现见下方页面代码中的数据源封装)
  }
}

为便于直接运行,下面给出一个自包含的完整页面示例:

收起
自动换行
深色代码主题
复制
// pages/NewsListPage.ets
// LazyForEach 触底加载更多,滚动位置不跳动
import { IDataSource, DataChangeListener } from '@kit.ArkUI';

// 业务数据模型,显式声明类型
class NewsItem {
  id: number = 0;
  title: string = '';

  constructor(id: number, title: string) {
    this.id = id;
    this.title = title;
  }
}

// 数据源:实现 IDataSource,追加数据使用 onDataAdd 精确通知
class NewsDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private dataArray: NewsItem[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): NewsItem {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos: number = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  // 尾部批量追加:逐条入数组并按位置精确通知,禁止调用 onDataReloaded
  appendItems(items: NewsItem[]): void {
    for (let i = 0; i < items.length; i++) {
      this.dataArray.push(items[i]);
      const index: number = this.dataArray.length - 1;
      this.listeners.forEach((listener: DataChangeListener) => {
        listener.onDataAdd(index);
      });
    }
  }
}

@Entry
@Component
struct NewsListPage {
  // 数据源不加 @State / @Local,避免引用级观测触发整体重建
  private dataSource: NewsDataSource = new NewsDataSource();
  private isLoading: boolean = false;
  private pageIndex: number = 0;

  aboutToAppear(): void {
    this.loadMore();
  }

  // 模拟分页请求
  private loadMore(): void {
    if (this.isLoading) {
      return;
    }
    this.isLoading = true;
    const startId: number = this.pageIndex * 30;
    const items: NewsItem[] = [];
    for (let i = 0; i < 30; i++) {
      const item: NewsItem = new NewsItem(startId + i, `新闻标题 ${startId + i}`);
      items.push(item);
    }
    // 追加成功后仅做精确通知,滚动位置保持不变
    this.dataSource.appendItems(items);
    this.pageIndex++;
    this.isLoading = false;
  }

  build() {
    List({ space: 8 }) {
      LazyForEach(this.dataSource, (item: NewsItem) => {
        ListItem() {
          Text(item.title)
            .fontSize(16)
            .padding(16)
            .width('100%')
        }
      }, (item: NewsItem) => item.id.toString()) // 键值使用业务 id,稳定且唯一
    }
    .width('100%')
    .height('100%')
    .cachedCount(5)
    .onReachEnd(() => {
      // 滑动到底部触发加载更多
      this.loadMore();
    })
  }
}

排查与补充说明

结合上述原理,建议在分包页面按以下顺序逐项核对:

  1. 检查加载更多成功回调中是否存在 notifyDataReload() 调用或数据源整体重新赋值,改为 onDataAdd 逐条精确通知。
  2. 检查数据源是否被 @State、@Local 等状态装饰器修饰,若有则移除,数据刷新完全交给数据源的通知机制。
  3. 检查 keyGenerator 是否返回 index 或包含时间戳等变化值,改为返回业务唯一 id。
  4. 若列表头部还有下拉刷新等会向头部插数据的场景,可配合 List 的 maintainVisibleContentPosition 属性保持可见内容位置;但注意该属性对 onDataReloaded 触发的全量重载无效,精确通知仍是前提。
  5. 分包页面首帧渲染尚未完成时不要提前触发数据重载,首次数据初始化与后续分页追加应走不同路径:初始化可在 aboutToAppear 中完成,追加一律走精确通知。

相关官方文档可参考 LazyForEach 数据懒加载与键值生成规则说明(developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-rendering-control-lazyforeach)。若按上述方式调整后分包页面仍出现跳顶,可重新发帖并附上数据源实现代码、LazyForEach 的 keyGenerator 写法以及 API 版本信息,以便进一步定位。

展开全部
1
1楼回复于4天前 来自广东
  • 匿名用户匿名用户
    根据AI的提示看下呢
    4天前 来自浙江
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要提问题

了解社区公约,与您携手共创和谐专业的开发者社区。