@tarojs/components-advanced 4.1.12-beta.3 → 4.1.12-beta.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/components/index.js +2 -0
  2. package/dist/components/index.js.map +1 -1
  3. package/dist/components/list/NoMore.d.ts +30 -0
  4. package/dist/components/list/NoMore.js +10 -0
  5. package/dist/components/list/NoMore.js.map +1 -0
  6. package/dist/components/list/hooks/useItemSizeCache.d.ts +13 -0
  7. package/dist/components/list/hooks/useItemSizeCache.js +40 -0
  8. package/dist/components/list/hooks/useItemSizeCache.js.map +1 -0
  9. package/dist/components/list/hooks/useListNestedScroll.d.ts +18 -0
  10. package/dist/components/list/hooks/useListNestedScroll.js +61 -0
  11. package/dist/components/list/hooks/useListNestedScroll.js.map +1 -0
  12. package/dist/components/list/hooks/useListScrollElementAttach.d.ts +25 -0
  13. package/dist/components/list/hooks/useListScrollElementAttach.js +88 -0
  14. package/dist/components/list/hooks/useListScrollElementAttach.js.map +1 -0
  15. package/dist/components/list/hooks/useListScrollElementAttachWeapp.d.ts +14 -0
  16. package/dist/components/list/hooks/useListScrollElementAttachWeapp.js +134 -0
  17. package/dist/components/list/hooks/useListScrollElementAttachWeapp.js.map +1 -0
  18. package/dist/components/list/hooks/useMeasureStartOffset.d.ts +12 -0
  19. package/dist/components/list/hooks/useMeasureStartOffset.js +84 -0
  20. package/dist/components/list/hooks/useMeasureStartOffset.js.map +1 -0
  21. package/dist/components/list/hooks/useMeasureStartOffsetWeapp.d.ts +14 -0
  22. package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js +82 -0
  23. package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js.map +1 -0
  24. package/dist/components/list/hooks/useRefresher.d.ts +73 -0
  25. package/dist/components/list/hooks/useRefresher.js +500 -0
  26. package/dist/components/list/hooks/useRefresher.js.map +1 -0
  27. package/dist/components/list/hooks/useResizeObserver.d.ts +27 -0
  28. package/dist/components/list/hooks/useResizeObserver.js +152 -0
  29. package/dist/components/list/hooks/useResizeObserver.js.map +1 -0
  30. package/dist/components/list/hooks/useScrollCorrection.d.ts +19 -0
  31. package/dist/components/list/hooks/useScrollCorrection.js +74 -0
  32. package/dist/components/list/hooks/useScrollCorrection.js.map +1 -0
  33. package/dist/components/list/hooks/useScrollParentAutoFind.d.ts +20 -0
  34. package/dist/components/list/hooks/useScrollParentAutoFind.js +81 -0
  35. package/dist/components/list/hooks/useScrollParentAutoFind.js.map +1 -0
  36. package/dist/components/list/index.d.ts +66 -7
  37. package/dist/components/list/index.js +1079 -162
  38. package/dist/components/list/index.js.map +1 -1
  39. package/dist/components/list/utils.d.ts +21 -0
  40. package/dist/components/list/utils.js +35 -0
  41. package/dist/components/list/utils.js.map +1 -0
  42. package/dist/components/virtual-list/vue/list.d.ts +12 -12
  43. package/dist/components/virtual-waterfall/vue/waterfall.d.ts +11 -11
  44. package/dist/components/water-flow/flow-item.js +6 -4
  45. package/dist/components/water-flow/flow-item.js.map +1 -1
  46. package/dist/components/water-flow/flow-section.js +1 -1
  47. package/dist/components/water-flow/flow-section.js.map +1 -1
  48. package/dist/components/water-flow/index.d.ts +1 -1
  49. package/dist/components/water-flow/interface.d.ts +18 -2
  50. package/dist/components/water-flow/root.d.ts +35 -4
  51. package/dist/components/water-flow/root.js +114 -42
  52. package/dist/components/water-flow/root.js.map +1 -1
  53. package/dist/components/water-flow/section.d.ts +7 -1
  54. package/dist/components/water-flow/section.js +54 -9
  55. package/dist/components/water-flow/section.js.map +1 -1
  56. package/dist/components/water-flow/utils.d.ts +4 -0
  57. package/dist/components/water-flow/utils.js +5 -1
  58. package/dist/components/water-flow/utils.js.map +1 -1
  59. package/dist/components/water-flow/water-flow-node-cache.d.ts +24 -0
  60. package/dist/components/water-flow/water-flow-node-cache.js +161 -0
  61. package/dist/components/water-flow/water-flow-node-cache.js.map +1 -0
  62. package/dist/components/water-flow/water-flow.d.ts +2 -3
  63. package/dist/components/water-flow/water-flow.js +286 -31
  64. package/dist/components/water-flow/water-flow.js.map +1 -1
  65. package/dist/index.js +3 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/utils/index.d.ts +1 -0
  68. package/dist/utils/index.js +1 -0
  69. package/dist/utils/index.js.map +1 -1
  70. package/dist/utils/scrollElementContext.d.ts +15 -0
  71. package/dist/utils/scrollElementContext.js +14 -0
  72. package/dist/utils/scrollElementContext.js.map +1 -0
  73. package/dist/utils/scrollParent.d.ts +33 -0
  74. package/dist/utils/scrollParent.js +88 -0
  75. package/dist/utils/scrollParent.js.map +1 -0
  76. package/package.json +9 -8
@@ -1,11 +1,23 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { View, ScrollView } from '@tarojs/components';
3
+ import Taro from '@tarojs/taro';
3
4
  import React from 'react';
5
+ import '../../utils/index.js';
6
+ import { ScrollElementContextOrFallback } from '../../utils/scrollElementContext.js';
7
+ import { useItemSizeCache } from './hooks/useItemSizeCache.js';
8
+ import { useListNestedScroll } from './hooks/useListNestedScroll.js';
9
+ import { useListScrollElementAttach } from './hooks/useListScrollElementAttach.js';
10
+ import { useListScrollElementAttachWeapp } from './hooks/useListScrollElementAttachWeapp.js';
11
+ import { useRefresher, DEFAULT_REFRESHER_HEIGHT } from './hooks/useRefresher.js';
12
+ import { useResizeObserver } from './hooks/useResizeObserver.js';
13
+ import { useScrollCorrection } from './hooks/useScrollCorrection.js';
4
14
  import { ListItem } from './ListItem.js';
15
+ import { NoMore } from './NoMore.js';
5
16
  import { StickyHeader } from './StickyHeader.js';
6
17
  import { StickySection } from './StickySection.js';
18
+ import { isWeapp, createSelectorQueryScoped, isH5, supportsNativeRefresher } from './utils.js';
19
+ import { getScrollViewContextNode } from '../../utils/dom.js';
7
20
 
8
- // 工具:累加数组
9
21
  function accumulate(arr) {
10
22
  const result = [0];
11
23
  for (let i = 0; i < arr.length; i++) {
@@ -13,46 +25,331 @@ function accumulate(arr) {
13
25
  }
14
26
  return result;
15
27
  }
16
- // 检测抖动
17
28
  function isShaking(diffList) {
18
29
  if (diffList.length < 3)
19
30
  return false;
20
- // 检查是否有连续的正负交替
21
31
  const signs = diffList.map(diff => Math.sign(diff));
22
32
  let alternations = 0;
23
33
  for (let i = 1; i < signs.length; i++) {
24
- if (signs[i] !== 0 && signs[i] !== signs[i - 1]) {
34
+ if (signs[i] !== 0 && signs[i] !== signs[i - 1])
25
35
  alternations++;
26
- }
27
36
  }
28
- // 如果交替次数过多,认为是抖动
29
37
  return alternations >= 2;
30
38
  }
31
- const List = (props) => {
32
- const isH5 = process.env.TARO_ENV === 'h5';
33
- const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, upperThreshold = 0, lowerThreshold = 0, cacheCount = 2, style, children, } = props;
39
+ // 小程序端:判断 item 是否应该执行 SelectorQuery 测量(仅检查是否已测量过)
40
+ // SelectorQuery 是只读查询,不会触发 setData,滚动期间执行是安全的
41
+ function shouldMeasureWeappItem(index, measuredSet) {
42
+ return !measuredSet.has(index);
43
+ }
44
+ // 小程序端暂不外抛 onItemSizeChange,避免父层重渲染导致 List remount 引发回顶或空白
45
+ function weappDeferItemSizeChange(_index, _size, _onItemSizeChange) { }
46
+ /** 从 scroll 选项解析目标偏移量 */
47
+ function resolveScrollTargetOffset(options, isHorizontal) {
48
+ const opts = options !== null && options !== void 0 ? options : {};
49
+ const top = typeof opts.top === 'number' ? opts.top : undefined;
50
+ const left = typeof opts.left === 'number' ? opts.left : undefined;
51
+ let result = 0;
52
+ if (isHorizontal) {
53
+ if (typeof left === 'number')
54
+ result = left;
55
+ else if (typeof top === 'number')
56
+ result = top;
57
+ }
58
+ else {
59
+ if (typeof top === 'number')
60
+ result = top;
61
+ else if (typeof left === 'number')
62
+ result = left;
63
+ }
64
+ return Number.isFinite(result) ? result : 0;
65
+ }
66
+ // eslint-disable-next-line complexity -- List 多端/多模式逻辑集中,已抽离 useListNestedScroll、useListScrollElementAttach、resolveScrollTargetOffset 等
67
+ const InnerList = (props, ref) => {
68
+ var _a, _b;
69
+ const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, onScrollStart, onScrollEnd, upperThreshold = 50, lowerThreshold = 50, cacheCount = 2, cacheExtent, enableBackToTop, className, style, children, nestedScroll, scrollElement, scrollRef: scrollRefProp, } = props;
34
70
  const isHorizontal = scrollX === true;
71
+ const listType = nestedScroll === true ? 'nested' : 'default';
72
+ const { effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, useScrollElementMode, needAutoFind, autoFindStatus, contentWrapperRef, contentId, } = useListNestedScroll(listType, scrollElement, undefined, isHorizontal, props.selectorQueryScope);
35
73
  const DEFAULT_ITEM_WIDTH = 120;
36
74
  const DEFAULT_ITEM_HEIGHT = 40;
75
+ const defaultItemSize = isHorizontal ? DEFAULT_ITEM_WIDTH : DEFAULT_ITEM_HEIGHT;
76
+ const normalizeSize = React.useCallback((value) => {
77
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
78
+ return null;
79
+ return value;
80
+ }, []);
81
+ const resolveItemSizeByIndex = React.useCallback((index, fallback) => {
82
+ const { itemSize, itemData } = props;
83
+ const numberSize = normalizeSize(itemSize);
84
+ if (numberSize != null)
85
+ return numberSize;
86
+ if (typeof itemSize === 'function') {
87
+ const functionSize = normalizeSize(itemSize(index, itemData));
88
+ if (functionSize != null)
89
+ return functionSize;
90
+ }
91
+ return fallback;
92
+ }, [props.itemSize, props.itemData, normalizeSize]);
37
93
  // 滚动状态管理
38
94
  const containerRef = React.useRef(null);
95
+ // 生成唯一 List ID(用于小程序 ResizeObserver)
96
+ const listId = React.useMemo(() => `list-${Math.random().toString(36).slice(2, 11)}`, []);
39
97
  // 渲染偏移量 - 用于计算应该渲染哪些元素
40
98
  const [renderOffset, setRenderOffset] = React.useState(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
41
- // 滚动视图偏移量 - 只在滚动结束或明确请求时更新到ScrollView
99
+ // 程序性滚动用的目标偏移;用户滑动期间不更新,避免与原生滚动冲突
42
100
  const [scrollViewOffset, setScrollViewOffset] = React.useState(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
43
- const [containerLength] = React.useState(typeof (isHorizontal ? width : height) === 'number' ? (isHorizontal ? width : height) : 400);
101
+ // 用户正在滑动时不再向 ScrollView scrollTop,让滚动完全由原生接管
102
+ const [isUserScrolling, setIsUserScrolling] = React.useState(false);
103
+ // isUserScrolling 的 ref 镜像,供异步上下文读取最新值
104
+ const isUserScrollingRef = React.useRef(false);
105
+ const initialContainerLength = typeof (isHorizontal ? width : height) === 'number' ? (isHorizontal ? width : height) : 400;
106
+ const [containerLength, setContainerLength] = React.useState(initialContainerLength);
107
+ // 用容器实际尺寸更新视口长度,避免 props 与 CSS 不一致导致底部空白
108
+ React.useEffect(() => {
109
+ const el = containerRef.current;
110
+ if (!el || typeof ResizeObserver === 'undefined')
111
+ return;
112
+ const ro = new ResizeObserver((entries) => {
113
+ for (const entry of entries) {
114
+ const { contentRect } = entry;
115
+ const measured = isHorizontal ? contentRect.width : contentRect.height;
116
+ if (measured > 0)
117
+ setContainerLength(measured);
118
+ }
119
+ });
120
+ ro.observe(el);
121
+ return () => ro.disconnect();
122
+ }, [isHorizontal]);
123
+ // WeChat 小程序:没有原生 ResizeObserver,用 SelectorQuery 一次性测量容器高度
124
+ React.useEffect(() => {
125
+ if (!isWeapp)
126
+ return;
127
+ Taro.nextTick(() => {
128
+ createSelectorQueryScoped(props.selectorQueryScope)
129
+ .select(`#${listId}`)
130
+ .boundingClientRect((rect) => {
131
+ const measured = isHorizontal ? rect === null || rect === void 0 ? void 0 : rect.width : rect === null || rect === void 0 ? void 0 : rect.height;
132
+ if (typeof measured === 'number' && measured > 0) {
133
+ setContainerLength(measured);
134
+ }
135
+ })
136
+ .exec();
137
+ });
138
+ }, [isWeapp, isHorizontal, listId]);
44
139
  // 滚动追踪相关refs
45
140
  const isScrollingRef = React.useRef(false);
46
141
  const lastScrollTopRef = React.useRef(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
47
142
  const scrollDiffListRef = React.useRef([0, 0, 0]);
48
143
  const scrollTimeoutRef = React.useRef(null);
49
- // 解析分组结构,只支持 StickySection 和 ListItem 作为直接子组件
144
+ // H5:仅程序性滚动时写回 DOM scrollTop,用户滑动结束后不写回避免卡顿
145
+ const programmaticScrollRef = React.useRef(false);
146
+ // 小程序端程序性滚动冷却期:handleScroll 只更新 renderOffset,避免 scrollIntoView 后被原生回调拉回
147
+ const programmaticCooldownRef = React.useRef(false);
148
+ const programmaticCooldownTimerRef = React.useRef(null);
149
+ const scrollViewOffsetRef = React.useRef(0);
150
+ // 用户滚动期间挂起的 reflow 标记(用于滚动结束后补触发)
151
+ const pendingWeappReflowRef = React.useRef(false);
152
+ // ref 持有最新版 scheduleWeappDynamicReflow,供 updateRenderOffset 内的 setTimeout 闭包安全访问
153
+ const scheduleWeappDynamicReflowRef = React.useRef(null);
154
+ // 处理渲染偏移量更新。
155
+ // syncToScrollView=true:程序性滚动,立即同步 scrollViewOffset。
156
+ // syncToScrollView=false:用户滑动,仅更新 renderOffset。weapp 采用 recycle-view 策略:不把用户滑动位置同步到 scrollViewOffset,避免「传滞后值拉回」和「从有到无归顶」。
157
+ const updateRenderOffset = React.useCallback((newOffset, syncToScrollView, source) => {
158
+ lastScrollTopRef.current = newOffset;
159
+ isScrollingRef.current = true;
160
+ if (scrollTimeoutRef.current) {
161
+ clearTimeout(scrollTimeoutRef.current);
162
+ }
163
+ setRenderOffset(newOffset); // 始终更新虚拟列表用到的偏移
164
+ if (syncToScrollView) {
165
+ isUserScrollingRef.current = false;
166
+ setIsUserScrolling(false);
167
+ // 小程序:target===sv 时 ScrollView 认为无变化不滚动→白屏;imperative/scrollIntoView 时先传中间值再 RAF 传 target 强制触发
168
+ // target=0 用 +0.01;target>0 用 -0.01(统一 +0.01 到底部会被 clamp 成同值无效)
169
+ const same = isWeapp && Math.abs(scrollViewOffsetRef.current - newOffset) < 1;
170
+ const needForce = same && (source === 'imperative' || source === 'scrollIntoView');
171
+ if (needForce) {
172
+ const intermediate = newOffset > 0 ? newOffset - 0.01 : 0.01;
173
+ setScrollViewOffset(intermediate);
174
+ programmaticScrollRef.current = true;
175
+ requestAnimationFrame(() => {
176
+ // 第二帧也需要标记为程序性滚动,否则 else 分支不会传 scrollTop
177
+ programmaticScrollRef.current = true;
178
+ setScrollViewOffset(newOffset);
179
+ });
180
+ }
181
+ else {
182
+ setScrollViewOffset(newOffset);
183
+ }
184
+ programmaticScrollRef.current = true;
185
+ if (isWeapp) {
186
+ programmaticCooldownRef.current = true;
187
+ if (programmaticCooldownTimerRef.current) {
188
+ clearTimeout(programmaticCooldownTimerRef.current);
189
+ }
190
+ programmaticCooldownTimerRef.current = setTimeout(() => {
191
+ programmaticCooldownRef.current = false;
192
+ }, 500);
193
+ }
194
+ }
195
+ else {
196
+ isUserScrollingRef.current = true;
197
+ setIsUserScrolling(true);
198
+ }
199
+ scrollTimeoutRef.current = setTimeout(() => {
200
+ isScrollingRef.current = false;
201
+ if (!syncToScrollView) {
202
+ isUserScrollingRef.current = false;
203
+ setIsUserScrolling(false);
204
+ // weapp recycle-view 策略:用户滑动结束后不同步 scrollViewOffset,保持 pass 的值不变,避免 从有到无 归顶
205
+ if (!isWeapp) {
206
+ setScrollViewOffset(lastScrollTopRef.current);
207
+ }
208
+ // 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
209
+ if (isWeapp && pendingWeappReflowRef.current) {
210
+ scheduleWeappDynamicReflowRef.current();
211
+ }
212
+ }
213
+ }, isWeapp ? 200 : 150);
214
+ }, []);
215
+ // 暴露给外部的实例方法:通过 ref.scroll({ top / left }) 进行程序性滚动
216
+ React.useImperativeHandle(ref, () => ({
217
+ scroll(options) {
218
+ const targetOffset = resolveScrollTargetOffset(options, isHorizontal);
219
+ const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
220
+ if (el && isH5) {
221
+ const scrollTarget = targetOffset + effectiveStartOffset;
222
+ if (isHorizontal) {
223
+ el.scrollTo({ left: scrollTarget });
224
+ }
225
+ else {
226
+ el.scrollTo({ top: scrollTarget });
227
+ }
228
+ updateRenderOffset(targetOffset, false, 'scrollElement');
229
+ }
230
+ else if (el && isWeapp && useScrollElementMode) {
231
+ // 小程序 scrollElement 模式:需通过 getScrollViewContextNode + node.scrollTo 真正滚动外部 scroll-view
232
+ // updateRenderOffset 必须在 scrollTo 之后调用,否则 getScrollViewContextNode 异步期间会先更新 renderOffset 导致闪一下
233
+ const startOff = effectiveStartOffsetRef.current;
234
+ const scrollTarget = targetOffset + startOff;
235
+ const scrollViewId = el.id || `_ls_${listId}`;
236
+ if (!el.id)
237
+ el.id = scrollViewId;
238
+ getScrollViewContextNode(`#${scrollViewId}`).then((node) => {
239
+ var _a, _b;
240
+ if (isHorizontal) {
241
+ (_a = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _a === void 0 ? void 0 : _a.call(node, { left: scrollTarget, animated: false });
242
+ }
243
+ else {
244
+ (_b = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _b === void 0 ? void 0 : _b.call(node, { top: scrollTarget, animated: false });
245
+ }
246
+ updateRenderOffset(targetOffset, true, 'imperative');
247
+ });
248
+ }
249
+ else {
250
+ updateRenderOffset(targetOffset, true, 'imperative');
251
+ }
252
+ },
253
+ }), [scrollX, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId, useScrollElementMode, updateRenderOffset]);
254
+ // 提取 Refresher 配置(List 属性为 base,Refresher 子组件覆盖)
255
+ const refresherConfig = React.useMemo(() => {
256
+ const listRefresherEnabled = props.refresherEnabled !== false && (props.refresherEnabled === true || props.onRefresherRefresh != null);
257
+ const baseFromList = listRefresherEnabled
258
+ ? {
259
+ refresherEnabled: props.refresherEnabled,
260
+ refresherThreshold: props.refresherThreshold,
261
+ refresherDefaultStyle: props.refresherDefaultStyle,
262
+ refresherBackground: props.refresherBackground,
263
+ refresherTriggered: props.refresherTriggered,
264
+ onRefresherPulling: props.onRefresherPulling,
265
+ onRefresherRefresh: props.onRefresherRefresh,
266
+ onRefresherRestore: props.onRefresherRestore,
267
+ onRefresherAbort: props.onRefresherAbort,
268
+ onRefresherWillRefresh: props.onRefresherWillRefresh,
269
+ onRefresherStatusChange: props.onRefresherStatusChange,
270
+ }
271
+ : null;
272
+ const isRefresherComponent = (child) => {
273
+ const type = child.type;
274
+ return (type === null || type === void 0 ? void 0 : type.displayName) === 'Refresher' || (type === null || type === void 0 ? void 0 : type.name) === 'Refresher';
275
+ };
276
+ let refresherChildProps = null;
277
+ let refresherCount = 0;
278
+ React.Children.forEach(children, (child) => {
279
+ if (React.isValidElement(child) && isRefresherComponent(child)) {
280
+ refresherCount++;
281
+ if (refresherCount > 1) {
282
+ return;
283
+ }
284
+ refresherChildProps = child.props;
285
+ }
286
+ });
287
+ if (refresherChildProps != null) {
288
+ const base = baseFromList !== null && baseFromList !== void 0 ? baseFromList : {};
289
+ // Refresher 子组件的配置覆盖 List 的配置
290
+ return Object.assign(Object.assign({}, base), refresherChildProps);
291
+ }
292
+ return baseFromList;
293
+ }, [
294
+ children,
295
+ props.refresherEnabled,
296
+ props.refresherThreshold,
297
+ props.refresherDefaultStyle,
298
+ props.refresherBackground,
299
+ props.refresherTriggered,
300
+ props.onRefresherPulling,
301
+ props.onRefresherRefresh,
302
+ props.onRefresherRestore,
303
+ props.onRefresherAbort,
304
+ props.onRefresherWillRefresh,
305
+ props.onRefresherStatusChange,
306
+ ]);
307
+ // 提取 NoMore 配置
308
+ const noMoreConfig = React.useMemo(() => {
309
+ var _a;
310
+ let config = null;
311
+ let noMoreCount = 0;
312
+ // 从子组件中提取 NoMore
313
+ React.Children.forEach(children, (child) => {
314
+ if (React.isValidElement(child) && child.type === NoMore) {
315
+ noMoreCount++;
316
+ if (noMoreCount > 1) {
317
+ return;
318
+ }
319
+ const childProps = child.props;
320
+ config = Object.assign(Object.assign({}, childProps), { visible: childProps.visible !== false });
321
+ }
322
+ });
323
+ // Props 方式转换为配置(优先级低于子组件)
324
+ if (props.showNoMore && !config) {
325
+ config = {
326
+ visible: true,
327
+ text: props.noMoreText,
328
+ style: props.noMoreStyle,
329
+ children: (_a = props.renderNoMore) === null || _a === void 0 ? void 0 : _a.call(props)
330
+ };
331
+ }
332
+ return config;
333
+ }, [children, props.showNoMore, props.noMoreText, props.noMoreStyle, props.renderNoMore]);
334
+ // Refresher 平台适配:H5 用 addImperativeTouchListeners
335
+ const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset, useScrollElementMode
336
+ ? (ev) => { var _a, _b; return (_b = (ev.target != null && ((_a = contentWrapperRef.current) === null || _a === void 0 ? void 0 : _a.contains(ev.target)))) !== null && _b !== void 0 ? _b : false; }
337
+ : undefined);
338
+ const refresherTeardownRef = React.useRef(null);
339
+ const addImperativeTouchListenersRef = React.useRef(addImperativeTouchListeners);
340
+ addImperativeTouchListenersRef.current = addImperativeTouchListeners;
341
+ React.useEffect(() => () => {
342
+ if (refresherTeardownRef.current)
343
+ refresherTeardownRef.current();
344
+ }, []);
345
+ // H5 下拉刷新顶栏高度(默认 50px,来自 useRefresher)
346
+ const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false) ? DEFAULT_REFRESHER_HEIGHT : 0;
347
+ // 解析分组结构:StickySection、ListItem 为直接子组件,过滤 Refresher/NoMore
50
348
  const sections = React.useMemo(() => {
51
349
  const result = [];
52
350
  const defaultItems = [];
53
351
  React.Children.forEach(children, (child, idx) => {
54
352
  if (React.isValidElement(child) && child.type === StickySection) {
55
- // 分组模式
56
353
  const sectionProps = child.props;
57
354
  let header = null;
58
355
  const items = [];
@@ -65,7 +362,6 @@ const List = (props) => {
65
362
  result.push({ header, items, key: child.key || String(idx) });
66
363
  }
67
364
  else if (React.isValidElement(child) && child.type === ListItem) {
68
- // 普通 ListItem
69
365
  defaultItems.push(child);
70
366
  }
71
367
  });
@@ -74,65 +370,169 @@ const List = (props) => {
74
370
  }
75
371
  return result;
76
372
  }, [children]);
77
- // 工具:获取 header 尺寸,确保所有 header 相关逻辑一致
78
- const getHeaderSize = () => {
79
- if (isHorizontal) {
80
- if (typeof props.headerWidth === 'number')
81
- return props.headerWidth;
82
- if (typeof props.itemWidth === 'number')
83
- return props.itemWidth;
84
- if (typeof props.itemSize === 'number')
85
- return props.itemSize;
86
- if (typeof props.itemSize === 'function')
87
- return props.itemSize(0, props.itemData) || DEFAULT_ITEM_WIDTH;
88
- return DEFAULT_ITEM_WIDTH;
373
+ // 动态尺寸管理
374
+ const estimatedSize = resolveItemSizeByIndex(0, defaultItemSize);
375
+ // 计算总 item 数量(跨所有 section)
376
+ const totalItemCount = React.useMemo(() => {
377
+ return sections.reduce((sum, section) => sum + section.items.length, 0);
378
+ }, [sections]);
379
+ // 存储元素引用(用于 ResizeObserver)
380
+ const itemRefsRef = React.useRef(new Map());
381
+ // 存储 header 元素引用(用于 ResizeObserver)
382
+ const headerRefsRef = React.useRef(new Map());
383
+ // 小程序端:已完成 SelectorQuery 测量的 item 索引集,避免 refCallback 重复触发测量导致无限循环
384
+ const weappMeasuredItemsRef = React.useRef(new Set());
385
+ // 动态尺寸缓存更新版本:setItemSize 后递增,用于驱动 sectionOffsets/totalLength 与 item 定位重算
386
+ const [sizeCacheVersion, setSizeCacheVersion] = React.useState(0);
387
+ const sizeCacheRafRef = React.useRef(null);
388
+ // (measureScrollProtectRef 已移除:scrollTop + 内容变更同帧 setData 会与 scrollAnchoring
389
+ // 冲突导致归顶;改由 scrollAnchoring=true 独立处理内容重排,无需显式传 scrollTop 保护)
390
+ // 动态尺寸缓存
391
+ const sizeCache = useItemSizeCache({
392
+ isHorizontal,
393
+ estimatedSize,
394
+ itemCount: totalItemCount
395
+ });
396
+ // header 动态尺寸缓存(sectionIndex -> size)
397
+ const headerSizeCacheRef = React.useRef(new Map());
398
+ // 滚动修正的可见起始索引
399
+ const visibleStartIndexRef = React.useRef(0);
400
+ // ScrollTop 修正(仅 H5):动高时尺寸变化自动修正 scrollTop
401
+ const scrollCorrectionEnabled = !isWeapp && props.useResizeObserver === true;
402
+ const scrollCorrection = useScrollCorrection({
403
+ enabled: scrollCorrectionEnabled,
404
+ visibleStartIndexRef,
405
+ setScrollOffset: (offsetOrUpdater) => {
406
+ const newOffset = typeof offsetOrUpdater === 'function'
407
+ ? offsetOrUpdater(renderOffset)
408
+ : offsetOrUpdater;
409
+ updateRenderOffset(newOffset, true, 'scrollCorrection'); // 程序性修正需同步到 ScrollView
89
410
  }
90
- else {
91
- if (typeof props.headerHeight === 'number')
92
- return props.headerHeight;
93
- if (typeof props.itemHeight === 'number')
94
- return props.itemHeight;
95
- if (typeof props.itemSize === 'number')
96
- return props.itemSize;
97
- if (typeof props.itemSize === 'function')
98
- return props.itemSize(0, props.itemData) || DEFAULT_ITEM_HEIGHT;
99
- return DEFAULT_ITEM_HEIGHT;
411
+ });
412
+ const scrollCorrectionRef = React.useRef(scrollCorrection);
413
+ scrollCorrectionRef.current = scrollCorrection;
414
+ const onScrollRef = React.useRef(onScroll);
415
+ onScrollRef.current = onScroll;
416
+ const onScrollToUpperRef = React.useRef(onScrollToUpper);
417
+ onScrollToUpperRef.current = onScrollToUpper;
418
+ const onScrollToLowerRef = React.useRef(onScrollToLower);
419
+ onScrollToLowerRef.current = onScrollToLower;
420
+ const thresholdRef = React.useRef({ upper: upperThreshold, lower: lowerThreshold });
421
+ thresholdRef.current = { upper: upperThreshold, lower: lowerThreshold };
422
+ const listContentLengthRef = React.useRef(0);
423
+ const inUpperZoneRef = React.useRef(true);
424
+ const inLowerZoneRef = React.useRef(false);
425
+ // 小程序 + 动高(virtual-list 风格):测量变化后同帧重排,不做程序性 scrollTop 回拉。
426
+ // 若用户正在滚动(惯性未结束),推迟到滚动停止后再触发,防止内容重排 + setData 同帧
427
+ // 导致 WeChat scrollAnchoring 失效或传滞后位置造成归顶。
428
+ const scheduleWeappDynamicReflow = React.useCallback(() => {
429
+ if (!isWeapp || props.useResizeObserver !== true)
430
+ return;
431
+ // 滚动中:仅标记为待处理,等 onScrollEnd / 200ms 超时触发
432
+ if (isUserScrollingRef.current) {
433
+ pendingWeappReflowRef.current = true;
434
+ return;
100
435
  }
101
- };
102
- // 工具:获取 item 尺寸,支持函数/props/默认值
103
- const getItemSize = (index) => {
104
- if (isHorizontal) {
105
- if (typeof props.itemWidth === 'number')
106
- return props.itemWidth;
107
- if (typeof props.itemSize === 'number')
108
- return props.itemSize;
109
- if (typeof props.itemSize === 'function')
110
- return props.itemSize(index, props.itemData) || DEFAULT_ITEM_WIDTH;
111
- return DEFAULT_ITEM_WIDTH;
436
+ if (sizeCacheRafRef.current != null)
437
+ return;
438
+ sizeCacheRafRef.current = requestAnimationFrame(() => {
439
+ sizeCacheRafRef.current = null;
440
+ pendingWeappReflowRef.current = false;
441
+ setSizeCacheVersion((v) => v + 1);
442
+ });
443
+ }, [isWeapp, props.useResizeObserver]);
444
+ // 每次渲染时更新 ref,保证 setTimeout 闭包内拿到最新版本
445
+ scheduleWeappDynamicReflowRef.current = scheduleWeappDynamicReflow;
446
+ // ResizeObserver(当启用动态测量时)
447
+ const resizeObserver = useResizeObserver({
448
+ enabled: props.useResizeObserver === true,
449
+ isHorizontal,
450
+ listId,
451
+ selectorQueryScope: props.selectorQueryScope,
452
+ onResize: (index, size) => {
453
+ var _a;
454
+ const oldSize = sizeCache.getItemSize(index);
455
+ sizeCache.setItemSize(index, size);
456
+ if (Math.abs(oldSize - size) >= 1) {
457
+ if (isWeapp && props.useResizeObserver === true) {
458
+ scheduleWeappDynamicReflow();
459
+ }
460
+ else if (sizeCacheRafRef.current == null) {
461
+ sizeCacheRafRef.current = requestAnimationFrame(() => {
462
+ sizeCacheRafRef.current = null;
463
+ setSizeCacheVersion((v) => v + 1);
464
+ });
465
+ }
466
+ }
467
+ // 触发 ScrollTop 修正
468
+ scrollCorrection.recordSizeChange(index, oldSize, size);
469
+ // 小程序:延迟 onItemSizeChange,避免父组件重渲染导致 List remount
470
+ // H5:直接回调
471
+ if (isWeapp) {
472
+ weappDeferItemSizeChange(index, size, props.onItemSizeChange);
473
+ }
474
+ else {
475
+ (_a = props.onItemSizeChange) === null || _a === void 0 ? void 0 : _a.call(props, index, size);
476
+ }
112
477
  }
113
- else {
114
- if (typeof props.itemHeight === 'number')
115
- return props.itemHeight;
116
- if (typeof props.itemSize === 'number')
117
- return props.itemSize;
118
- if (typeof props.itemSize === 'function')
119
- return props.itemSize(index, props.itemData) || DEFAULT_ITEM_HEIGHT;
120
- return DEFAULT_ITEM_HEIGHT;
478
+ });
479
+ // 嵌套滚动:内层高度变化上报
480
+ const handleReportNestedHeightChange = React.useCallback((height, index) => {
481
+ if (height <= 0)
482
+ return;
483
+ const estimatedHeader = estimatedSize * 0.5;
484
+ const fullHeight = height + estimatedHeader;
485
+ const oldSize = sizeCache.getItemSize(index);
486
+ if (Math.abs(oldSize - fullHeight) < 1)
487
+ return;
488
+ sizeCache.setItemSize(index, fullHeight);
489
+ scrollCorrection.recordSizeChange(index, oldSize, fullHeight);
490
+ if (isWeapp && props.useResizeObserver === true) {
491
+ scheduleWeappDynamicReflow();
121
492
  }
122
- };
123
- // 计算分组累积高度/宽度
493
+ else if (sizeCacheRafRef.current == null) {
494
+ sizeCacheRafRef.current = requestAnimationFrame(() => {
495
+ sizeCacheRafRef.current = null;
496
+ setSizeCacheVersion((v) => v + 1);
497
+ });
498
+ }
499
+ }, [sizeCache, scrollCorrection, estimatedSize, isWeapp, props.useResizeObserver, scheduleWeappDynamicReflow]);
500
+ const getDefaultHeaderSize = React.useCallback(() => {
501
+ const headerSize = normalizeSize(props.headerSize);
502
+ if (headerSize != null)
503
+ return headerSize;
504
+ return resolveItemSizeByIndex(0, defaultItemSize);
505
+ }, [props.headerSize, resolveItemSizeByIndex, defaultItemSize, normalizeSize]);
506
+ // 获取 header 尺寸(支持动态测量)
507
+ const getHeaderSize = React.useCallback((sectionIndex) => {
508
+ if (props.useResizeObserver === true) {
509
+ const cached = headerSizeCacheRef.current.get(sectionIndex);
510
+ if (cached != null && cached > 0)
511
+ return cached;
512
+ }
513
+ return getDefaultHeaderSize();
514
+ }, [props.useResizeObserver, getDefaultHeaderSize]);
515
+ const getItemSize = React.useCallback((index) => {
516
+ if (props.useResizeObserver === true) {
517
+ return sizeCache.getItemSize(index);
518
+ }
519
+ return resolveItemSizeByIndex(index, defaultItemSize);
520
+ }, [props.useResizeObserver, sizeCache, resolveItemSizeByIndex, defaultItemSize]);
521
+ // 分组累积高度/宽度,sizeCacheVersion 变化时重算
124
522
  const sectionOffsets = React.useMemo(() => {
125
523
  const offsets = [0];
126
- sections.forEach((section) => {
127
- const headerSize = getHeaderSize();
128
- const itemSizes = section.items.map((_, i) => getItemSize(i));
524
+ let globalItemIndex = 0;
525
+ sections.forEach((section, sectionIdx) => {
526
+ const headerSize = getHeaderSize(sectionIdx);
527
+ const itemSizes = section.items.map((_, localIdx) => getItemSize(globalItemIndex + localIdx));
129
528
  const groupSize = (section.header ? headerSize : 0) +
130
529
  itemSizes.reduce((a, b) => a + b, 0) +
131
530
  Math.max(0, section.items.length) * space;
132
531
  offsets.push(offsets[offsets.length - 1] + groupSize);
532
+ globalItemIndex += section.items.length;
133
533
  });
134
534
  return offsets;
135
- }, [sections, space, isHorizontal, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData]);
535
+ }, [sections, space, getItemSize, getHeaderSize, sizeCacheVersion]);
136
536
  // 外层虚拟滚动:可见分组
137
537
  const [startSection, endSection] = React.useMemo(() => {
138
538
  let start = 0;
@@ -151,119 +551,343 @@ const List = (props) => {
151
551
  }
152
552
  return [start, end];
153
553
  }, [renderOffset, containerLength, sectionOffsets, sections.length, cacheCount]);
154
- // 触顶/触底事件
155
- React.useEffect(() => {
156
- if (onScrollToUpper && renderOffset <= (upperThreshold > 0 ? sectionOffsets[upperThreshold] : 0)) {
157
- onScrollToUpper();
158
- }
159
- if (onScrollToLower && renderOffset + containerLength >= sectionOffsets[sectionOffsets.length - 1] - (lowerThreshold > 0 ? sectionOffsets[sectionOffsets.length - 1] - sectionOffsets[sections.length - lowerThreshold] : 0)) {
160
- onScrollToLower();
161
- }
162
- }, [renderOffset, containerLength, sectionOffsets, sections.length, upperThreshold, lowerThreshold, onScrollToUpper, onScrollToLower]);
163
- // 处理渲染偏移量更新
164
- const updateRenderOffset = React.useCallback((newOffset) => {
165
- lastScrollTopRef.current = newOffset;
166
- isScrollingRef.current = true;
167
- if (scrollTimeoutRef.current) {
168
- clearTimeout(scrollTimeoutRef.current);
169
- }
170
- setRenderOffset(newOffset); // 立即更新渲染偏移量
171
- // 平台适配:微信小程序使用延时,其他平台立即更新
172
- const isWeapp = process.env.TARO_ENV === 'weapp';
173
- if (isWeapp) {
174
- // 微信小程序:使用延时避免抖动
175
- scrollTimeoutRef.current = setTimeout(() => {
176
- isScrollingRef.current = false;
177
- setScrollViewOffset(newOffset); // 滚动结束后,同步滚动视图偏移量
178
- }, 200);
554
+ // 视口内可见 item 的全局索引范围(供 onScrollIndex)
555
+ const [visibleStartItem, visibleEndItem] = React.useMemo(() => {
556
+ const viewportTop = renderOffset;
557
+ const viewportBottom = renderOffset + containerLength;
558
+ let firstVisible = -1;
559
+ let lastVisible = -1;
560
+ let globalIndex = 0;
561
+ for (let s = 0; s < sections.length; s++) {
562
+ const section = sections[s];
563
+ const headerSize = getHeaderSize(s);
564
+ const sectionStart = sectionOffsets[s] + (section.header ? headerSize : 0);
565
+ let itemTop = sectionStart;
566
+ for (let i = 0; i < section.items.length; i++) {
567
+ const itemSize = getItemSize(globalIndex);
568
+ const itemBottom = itemTop + itemSize;
569
+ // 判断 item 本身(不含 space)是否与视口相交
570
+ if (itemBottom > viewportTop && itemTop < viewportBottom) {
571
+ if (firstVisible < 0)
572
+ firstVisible = globalIndex;
573
+ lastVisible = globalIndex;
574
+ }
575
+ // 下一个 item 的起始位置 = 当前 item 结束 + space
576
+ itemTop = itemBottom + space;
577
+ globalIndex++;
578
+ }
179
579
  }
180
- else {
181
- // 其他平台:立即更新以获得更好的响应性
182
- setScrollViewOffset(newOffset); // 立即更新滚动视图偏移量
183
- scrollTimeoutRef.current = setTimeout(() => {
184
- isScrollingRef.current = false;
185
- }, 200);
580
+ if (firstVisible < 0 || lastVisible < 0)
581
+ return [0, 0];
582
+ return [firstVisible, lastVisible];
583
+ }, [renderOffset, containerLength, sections, sectionOffsets, getHeaderSize, getItemSize, space]);
584
+ const lastVisibleRangeRef = React.useRef({ start: -1, end: -1 });
585
+ React.useEffect(() => {
586
+ if (props.onScrollIndex) {
587
+ if (lastVisibleRangeRef.current.start !== visibleStartItem ||
588
+ lastVisibleRangeRef.current.end !== visibleEndItem) {
589
+ lastVisibleRangeRef.current = { start: visibleStartItem, end: visibleEndItem };
590
+ props.onScrollIndex(visibleStartItem, visibleEndItem);
591
+ }
186
592
  }
187
- }, []);
188
- // 智能滚动处理函数
593
+ }, [visibleStartItem, visibleEndItem, props.onScrollIndex]);
189
594
  const handleScroll = React.useCallback((e) => {
190
- // 兼容Stencil版本和React版本的事件结构
595
+ var _a, _b, _c, _d;
191
596
  let newOffset;
192
597
  if (e.detail) {
193
- // React版本的事件结构
194
598
  newOffset = isHorizontal ? e.detail.scrollLeft : e.detail.scrollTop;
195
599
  }
196
600
  else {
197
- // Stencil版本的事件结构
198
601
  newOffset = isHorizontal ? e.scrollLeft : e.scrollTop;
199
602
  }
200
- const diff = newOffset - lastScrollTopRef.current;
603
+ const effectiveOffset = newOffset;
604
+ const currentThreshold = thresholdRef.current;
605
+ const { upper, lower } = currentThreshold;
606
+ // WeChat 小程序节点不具备 clientHeight/clientWidth,需显式回退到 containerLength state
607
+ const rawContainerSize = containerRef.current
608
+ ? (isHorizontal ? containerRef.current.clientWidth : containerRef.current.clientHeight)
609
+ : null;
610
+ const currentContainerLength = (typeof rawContainerSize === 'number' && rawContainerSize > 0)
611
+ ? rawContainerSize
612
+ : containerLength;
613
+ const nowInUpper = effectiveOffset <= upper;
614
+ const currentContentLength = listContentLengthRef.current;
615
+ const nowInLower = currentContentLength > 0 && effectiveOffset + currentContainerLength >= currentContentLength - lower;
616
+ const diff = effectiveOffset - lastScrollTopRef.current;
201
617
  scrollDiffListRef.current.shift();
202
618
  scrollDiffListRef.current.push(diff);
203
- // 只保留抖动检测,移除方向检测
204
- if (isScrollingRef.current && isShaking(scrollDiffListRef.current)) {
619
+ const shaking = isScrollingRef.current && isShaking(scrollDiffListRef.current);
620
+ if (shaking) {
205
621
  return;
206
622
  }
207
- updateRenderOffset(newOffset); // 直接更新渲染偏移量
208
- onScroll === null || onScroll === void 0 ? void 0 : onScroll({
209
- scrollTop: isHorizontal ? 0 : newOffset,
210
- scrollLeft: isHorizontal ? newOffset : 0
211
- });
212
- }, [isHorizontal, onScroll, updateRenderOffset, containerLength]);
213
- // 初始化后的延迟同步 - 确保ScrollView正确设置初始位置
623
+ if (programmaticCooldownRef.current) {
624
+ lastScrollTopRef.current = effectiveOffset;
625
+ setRenderOffset(effectiveOffset);
626
+ const scrollTop = isHorizontal ? 0 : newOffset;
627
+ const scrollLeft = isHorizontal ? newOffset : 0;
628
+ onScroll === null || onScroll === void 0 ? void 0 : onScroll({ scrollTop, scrollLeft, detail: { scrollTop, scrollLeft } });
629
+ if (nowInUpper && !inUpperZoneRef.current)
630
+ (_a = onScrollToUpperRef.current) === null || _a === void 0 ? void 0 : _a.call(onScrollToUpperRef);
631
+ if (nowInLower && !inLowerZoneRef.current)
632
+ (_b = onScrollToLowerRef.current) === null || _b === void 0 ? void 0 : _b.call(onScrollToLowerRef);
633
+ inUpperZoneRef.current = nowInUpper;
634
+ inLowerZoneRef.current = nowInLower;
635
+ return;
636
+ }
637
+ scrollCorrection.markUserScrolling();
638
+ updateRenderOffset(effectiveOffset, false, 'onScroll');
639
+ const scrollTop = isHorizontal ? 0 : newOffset;
640
+ const scrollLeft = isHorizontal ? newOffset : 0;
641
+ onScroll === null || onScroll === void 0 ? void 0 : onScroll({ scrollTop, scrollLeft, detail: { scrollTop, scrollLeft } });
642
+ if (nowInUpper && !inUpperZoneRef.current)
643
+ (_c = onScrollToUpperRef.current) === null || _c === void 0 ? void 0 : _c.call(onScrollToUpperRef);
644
+ if (nowInLower && !inLowerZoneRef.current)
645
+ (_d = onScrollToLowerRef.current) === null || _d === void 0 ? void 0 : _d.call(onScrollToLowerRef);
646
+ inUpperZoneRef.current = nowInUpper;
647
+ inLowerZoneRef.current = nowInLower;
648
+ }, [isHorizontal, onScroll, updateRenderOffset, scrollCorrection, props.useResizeObserver, containerLength]);
649
+ // 小程序:onScrollEnd 优先结束 isUserScrolling,timeout 兜底
650
+ const handleNativeScrollEnd = React.useCallback(() => {
651
+ if (isWeapp) {
652
+ if (scrollTimeoutRef.current) {
653
+ clearTimeout(scrollTimeoutRef.current);
654
+ scrollTimeoutRef.current = null;
655
+ }
656
+ if (isUserScrollingRef.current) {
657
+ isScrollingRef.current = false;
658
+ isUserScrollingRef.current = false;
659
+ setIsUserScrolling(false);
660
+ // 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
661
+ if (pendingWeappReflowRef.current) {
662
+ scheduleWeappDynamicReflowRef.current();
663
+ }
664
+ }
665
+ }
666
+ onScrollEnd === null || onScrollEnd === void 0 ? void 0 : onScrollEnd();
667
+ }, [isWeapp, onScrollEnd]);
668
+ // 暴露 scrollRef 给父组件,供内层 List/WaterFlow 传入 scrollElement
669
+ React.useLayoutEffect(() => {
670
+ if (!scrollRefProp)
671
+ return;
672
+ const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
673
+ if (el) {
674
+ scrollRefProp.current = el;
675
+ }
676
+ }, [scrollRefProp, useScrollElementMode, effectiveScrollElement]);
677
+ // controlledScrollTop 变化时同步到 ScrollView
214
678
  React.useEffect(() => {
215
679
  if (typeof controlledScrollTop === 'number') {
216
- setScrollViewOffset(controlledScrollTop);
680
+ const sv = scrollViewOffsetRef.current;
681
+ const same = isWeapp && Math.abs(sv - controlledScrollTop) < 1;
682
+ if (same) {
683
+ const intermediate = controlledScrollTop > 0 ? controlledScrollTop - 0.01 : 0.01;
684
+ setRenderOffset(intermediate);
685
+ setScrollViewOffset(intermediate);
686
+ requestAnimationFrame(() => {
687
+ setRenderOffset(controlledScrollTop);
688
+ setScrollViewOffset(controlledScrollTop);
689
+ });
690
+ }
691
+ else {
692
+ setRenderOffset(controlledScrollTop);
693
+ setScrollViewOffset(controlledScrollTop);
694
+ }
217
695
  lastScrollTopRef.current = controlledScrollTop;
696
+ programmaticScrollRef.current = true;
218
697
  }
219
698
  }, [controlledScrollTop]);
220
- // 清理定时器
699
+ // 清理定时器、ResizeObserver 与尺寸缓存 RAF(仅在卸载时执行)
221
700
  React.useEffect(() => {
222
701
  return () => {
223
702
  if (scrollTimeoutRef.current) {
224
703
  clearTimeout(scrollTimeoutRef.current);
225
704
  }
705
+ // 小程序端冷却期定时器清理
706
+ if (isWeapp && programmaticCooldownTimerRef.current) {
707
+ clearTimeout(programmaticCooldownTimerRef.current);
708
+ }
709
+ if (sizeCacheRafRef.current != null) {
710
+ cancelAnimationFrame(sizeCacheRafRef.current);
711
+ sizeCacheRafRef.current = null;
712
+ }
713
+ resizeObserver.disconnect();
714
+ scrollCorrection.clearQueue();
226
715
  };
227
716
  }, []);
228
- // 容器样式
229
- const containerStyle = Object.assign({ position: 'relative', boxSizing: 'border-box', height: isHorizontal ? width : height, width: isHorizontal ? height : width }, style);
230
- // 修改ScrollView组件的props,添加data-testid属性
231
- // ScrollView 属性
232
- const scrollViewProps = {
233
- scrollY: !scrollX && scrollY,
234
- scrollX: scrollX,
235
- style: containerStyle,
236
- enhanced: true,
237
- showScrollbar: showScrollbar,
238
- onScroll: handleScroll,
239
- onScrollToUpper,
240
- onScrollToLower,
241
- 'data-testid': 'taro-list-container'
242
- };
243
- // 设置ScrollView的滚动位置 - 同时兼容React版本和Stencil版本
244
- if (isHorizontal) {
245
- scrollViewProps.scrollLeft = scrollViewOffset; // React版本
246
- scrollViewProps.mpScrollLeft = scrollViewOffset; // Stencil版本
247
- }
248
- else {
249
- scrollViewProps.scrollTop = scrollViewOffset; // React版本
250
- scrollViewProps.mpScrollTop = scrollViewOffset; // Stencil版本
251
- }
252
- // H5上额外使用DOM直接操作确保滚动位置正确
253
- if (isH5) {
254
- React.useEffect(() => {
255
- if (containerRef.current && typeof scrollViewOffset === 'number') {
256
- if (isHorizontal) {
257
- containerRef.current.scrollLeft = scrollViewOffset;
717
+ // scrollIntoView:仅当 scrollIntoView 变化时执行一次跳动,统一转换为 scrollTop 路径(updateRenderOffset)。
718
+ const lastScrollIntoViewRef = React.useRef(null);
719
+ React.useEffect(() => {
720
+ const targetId = props.scrollIntoView;
721
+ if (!targetId) {
722
+ lastScrollIntoViewRef.current = null;
723
+ return;
724
+ }
725
+ if (lastScrollIntoViewRef.current === targetId)
726
+ return;
727
+ lastScrollIntoViewRef.current = targetId;
728
+ let targetIndex = -1;
729
+ if (targetId.startsWith('list-item-')) {
730
+ targetIndex = parseInt(targetId.replace('list-item-', ''), 10);
731
+ }
732
+ if (targetIndex >= 0 && targetIndex < totalItemCount) {
733
+ let targetOffset = 0;
734
+ let currentGlobalIndex = 0;
735
+ for (let s = 0; s < sections.length; s++) {
736
+ const section = sections[s];
737
+ const headerSize = section.header ? getHeaderSize(s) : 0;
738
+ if (currentGlobalIndex + section.items.length > targetIndex) {
739
+ targetOffset += headerSize;
740
+ for (let i = 0; i < targetIndex - currentGlobalIndex; i++) {
741
+ targetOffset += getItemSize(currentGlobalIndex + i) + space;
742
+ }
743
+ break;
258
744
  }
259
745
  else {
260
- containerRef.current.scrollTop = scrollViewOffset;
746
+ targetOffset += headerSize;
747
+ for (let i = 0; i < section.items.length; i++) {
748
+ targetOffset += getItemSize(currentGlobalIndex + i) + space;
749
+ }
750
+ currentGlobalIndex += section.items.length;
751
+ }
752
+ }
753
+ const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
754
+ if (useScrollElementMode && el) {
755
+ const scrollTarget = targetOffset + (isWeapp ? effectiveStartOffsetRef.current : effectiveStartOffset);
756
+ if (isH5) {
757
+ if (isHorizontal) {
758
+ el.scrollTo({ left: scrollTarget });
759
+ }
760
+ else {
761
+ el.scrollTo({ top: scrollTarget });
762
+ }
763
+ updateRenderOffset(targetOffset, true, 'scrollIntoView');
764
+ }
765
+ else if (isWeapp) {
766
+ const scrollViewId = el.id || `_ls_${listId}`;
767
+ if (!el.id)
768
+ el.id = scrollViewId;
769
+ getScrollViewContextNode(`#${scrollViewId}`).then((node) => {
770
+ var _a, _b;
771
+ if (isHorizontal) {
772
+ (_a = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _a === void 0 ? void 0 : _a.call(node, { left: scrollTarget, animated: false });
773
+ }
774
+ else {
775
+ (_b = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _b === void 0 ? void 0 : _b.call(node, { top: scrollTarget, animated: false });
776
+ }
777
+ updateRenderOffset(targetOffset, true, 'scrollIntoView');
778
+ });
261
779
  }
780
+ else {
781
+ updateRenderOffset(targetOffset, true, 'scrollIntoView');
782
+ }
783
+ }
784
+ else {
785
+ updateRenderOffset(targetOffset, true, 'scrollIntoView');
786
+ }
787
+ }
788
+ }, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset, useScrollElementMode, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId]);
789
+ // 容器样式;H5 刷新中禁止滚动
790
+ const containerStyle = Object.assign(Object.assign({ position: 'relative', boxSizing: 'border-box', height,
791
+ width }, style), (isH5 && refresherConfig && !supportsNativeRefresher && h5RefresherProps.isRefreshing
792
+ ? { overflow: 'hidden' }
793
+ : {}));
794
+ // ScrollView 属性
795
+ const scrollViewProps = Object.assign(Object.assign(Object.assign(Object.assign({ scrollY: !scrollX && scrollY, scrollX, style: containerStyle, className, enhanced: true, showScrollbar,
796
+ upperThreshold,
797
+ lowerThreshold, scrollWithAnimation: false, onScroll: handleScroll, onScrollStart, onScrollEnd: handleNativeScrollEnd, enableBackToTop }, (isWeapp ? { scrollAnchoring: true } : {})), (typeof cacheExtent === 'number' ? { cacheExtent } : {})), scrollViewRefresherProps), scrollViewRefresherHandlers);
798
+ // H5 对齐小程序:refresherTriggered=true 时顶部立即显示加载指示器,滚到顶部并锁定滚动直至设为 false
799
+ React.useEffect(() => {
800
+ if (!isH5 || !refresherConfig || supportsNativeRefresher || !h5RefresherProps.isRefreshing)
801
+ return;
802
+ updateRenderOffset(0, true, 'refresher');
803
+ }, [h5RefresherProps.isRefreshing, isH5, refresherConfig, supportsNativeRefresher, updateRenderOffset]);
804
+ // H5 下拉刷新:ref 存 addImperativeTouchListeners,避免 config 变化导致 effect 循环
805
+ React.useLayoutEffect(() => {
806
+ const attach = addImperativeTouchListenersRef.current;
807
+ if (!attach)
808
+ return;
809
+ let teardown = null;
810
+ const tryAttach = () => {
811
+ const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
812
+ if (el && !refresherTeardownRef.current) {
813
+ teardown = attach(el);
814
+ refresherTeardownRef.current = teardown;
815
+ }
816
+ };
817
+ tryAttach();
818
+ const rafId = requestAnimationFrame(tryAttach);
819
+ return () => {
820
+ cancelAnimationFrame(rafId);
821
+ if (teardown) {
822
+ teardown();
823
+ }
824
+ refresherTeardownRef.current = null;
825
+ };
826
+ }, [useScrollElementMode, effectiveScrollElement]);
827
+ scrollViewOffsetRef.current = scrollViewOffset;
828
+ // scrollTop 传递策略(对齐 virtual-list enhanced=true 的做法):
829
+ // - 程序性滚动帧(programmaticScrollRef=true):传 scrollViewOffset(目标位置)
830
+ // - 其余所有帧(用户滚动 / 测量重排 / 空闲):完全不传,依赖微信原生 + scrollAnchoring
831
+ // scrollAnchoring=true 负责在内容重排时自动维持视口位置,避免与显式 scrollTop
832
+ // 同帧冲突(冲突会导致 scrollAnchoring 失去锚点后归顶)
833
+ if (isWeapp) {
834
+ if (programmaticScrollRef.current) {
835
+ // 程序性滚动(imperative / scrollCorrection / scrollIntoView 等):传目标位置
836
+ if (isHorizontal) {
837
+ scrollViewProps.scrollLeft = scrollViewOffset;
262
838
  }
263
- }, [scrollViewOffset, isHorizontal]);
839
+ else {
840
+ scrollViewProps.scrollTop = scrollViewOffset;
841
+ }
842
+ programmaticScrollRef.current = false;
843
+ }
844
+ // else:用户滚动 / 空闲帧不传 scrollTop,让微信原生 + scrollAnchoring 完全接管
264
845
  }
265
- // 总高度/宽度
266
- const totalLength = sectionOffsets[sectionOffsets.length - 1];
846
+ else {
847
+ // H5:非用户滑动时传
848
+ if (!isUserScrolling) {
849
+ if (isHorizontal) {
850
+ scrollViewProps.scrollLeft = scrollViewOffset;
851
+ }
852
+ else {
853
+ scrollViewProps.scrollTop = scrollViewOffset;
854
+ }
855
+ }
856
+ }
857
+ // H5:程序性滚动时写回 DOM scrollTop
858
+ React.useEffect(() => {
859
+ if (!isH5)
860
+ return;
861
+ if (isUserScrolling || !containerRef.current || typeof scrollViewOffset !== 'number')
862
+ return;
863
+ if (!programmaticScrollRef.current)
864
+ return;
865
+ const scrollValue = scrollViewOffset;
866
+ if (isHorizontal) {
867
+ containerRef.current.scrollLeft = scrollValue;
868
+ }
869
+ else {
870
+ containerRef.current.scrollTop = scrollValue;
871
+ }
872
+ programmaticScrollRef.current = false;
873
+ }, [isH5, scrollViewOffset, isHorizontal, isUserScrolling]);
874
+ // 总高度/宽度(含 NoMore)
875
+ const noMoreHeight = (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible) ? (noMoreConfig.height || 60) : 0;
876
+ const listContentLength = sectionOffsets[sectionOffsets.length - 1] + noMoreHeight;
877
+ const totalLength = listContentLength;
878
+ // scrollElement 模式下 onScrollToLower 需用内层内容高度判断,供 scroll handler 读取
879
+ listContentLengthRef.current = listContentLength;
880
+ const scrollAttachRefsRef = React.useRef(null);
881
+ scrollAttachRefsRef.current = {
882
+ scrollCorrection: scrollCorrectionRef.current,
883
+ onScroll: onScrollRef.current,
884
+ onScrollToUpper: onScrollToUpperRef.current,
885
+ onScrollToLower: onScrollToLowerRef.current,
886
+ threshold: thresholdRef.current,
887
+ listContentLength: listContentLengthRef.current,
888
+ };
889
+ useListScrollElementAttach(useScrollElementMode && isH5, effectiveScrollElement, effectiveStartOffset, isHorizontal, setContainerLength, updateRenderOffset, scrollRefProp, scrollAttachRefsRef);
890
+ useListScrollElementAttachWeapp(useScrollElementMode && isWeapp, effectiveScrollElement, effectiveStartOffsetRef, effectiveStartOffset, isHorizontal, setContainerLength, updateRenderOffset, scrollRefProp, scrollAttachRefsRef, initialContainerLength, props.selectorQueryScope);
267
891
  // 吸顶/吸左 header
268
892
  const stickyHeaderNode = React.useMemo(() => {
269
893
  if (!stickyHeader)
@@ -272,33 +896,127 @@ const List = (props) => {
272
896
  if (sectionOffsets[i] <= renderOffset && renderOffset < sectionOffsets[i + 1]) {
273
897
  const section = sections[i];
274
898
  if (section.header) {
275
- const headerSize = getHeaderSize();
276
- // 内联样式替代className
277
- const stickyHeaderStyle = Object.assign({ position: 'sticky', top: 0, left: 0, zIndex: 100, background: '#fff', boxSizing: 'border-box', minHeight: '20px', overflow: 'hidden', lineHeight: 1 }, (isHorizontal ? { width: headerSize } : { height: headerSize }));
899
+ // 吸顶 header 不设固定 height/width,由内容撑开,避免「外部 60px 容器 + 实际内容 40+px」导致 header 内空白
900
+ const stickyHeaderStyle = {
901
+ position: 'sticky',
902
+ top: 0,
903
+ left: 0,
904
+ zIndex: 100,
905
+ background: '#fff',
906
+ boxSizing: 'border-box',
907
+ minHeight: 20,
908
+ overflow: 'hidden',
909
+ lineHeight: 1
910
+ };
278
911
  return (jsx(View, { style: stickyHeaderStyle, children: section.header }));
279
912
  }
280
913
  }
281
914
  }
282
915
  return null;
283
- }, [stickyHeader, renderOffset, sectionOffsets, sections, isHorizontal, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData]);
916
+ }, [stickyHeader, renderOffset, sectionOffsets, sections]);
284
917
  // 渲染分组+item双层虚拟滚动
285
918
  const renderSections = () => {
286
919
  const nodes = [];
287
920
  let offset = sectionOffsets[startSection];
921
+ let globalItemIndex = 0; // 全局 item 索引(跨 section)
922
+ // 计算起始 section 之前的所有 item 数量
923
+ for (let s = 0; s < startSection; s++) {
924
+ globalItemIndex += sections[s].items.length;
925
+ }
288
926
  for (let s = startSection; s <= endSection; s++) {
289
927
  const section = sections[s];
290
- const headerSize = getHeaderSize();
291
- const itemSizes = section.items.map((_, i) => getItemSize(i));
928
+ const headerSize = getHeaderSize(s);
929
+ const itemSizes = section.items.map((_, i) => getItemSize(globalItemIndex + i));
292
930
  // header
293
931
  if (section.header) {
294
- // 内联样式替代className
932
+ const sectionIndex = s;
933
+ // 动态测量时外层定位容器不设固定高度,由内层撑开
295
934
  const sectionHeaderStyle = Object.assign({ position: 'absolute', zIndex: 2, boxSizing: 'border-box', width: '100%', minHeight: '20px', overflow: 'hidden', lineHeight: 1 }, (isHorizontal
296
- ? { top: 0, height: '100%', left: offset, width: headerSize }
297
- : { top: offset, height: headerSize }));
298
- nodes.push(React.createElement(View, {
299
- key: section.key + '-header',
300
- style: sectionHeaderStyle,
301
- }, section.header));
935
+ ? { top: 0, height: '100%', left: offset, width: props.useResizeObserver ? undefined : headerSize }
936
+ : { top: offset, height: props.useResizeObserver ? undefined : headerSize }));
937
+ // header ref 回调(用于 ResizeObserver 测量)
938
+ const headerRefCallback = props.useResizeObserver ? (el) => {
939
+ if (el) {
940
+ headerRefsRef.current.set(sectionIndex, el);
941
+ resizeObserver.observe(el, -sectionIndex - 1); // 用负数索引区分 header 和 item
942
+ const measureAndUpdateHeader = (measured) => {
943
+ var _a;
944
+ if (measured > 0) {
945
+ const oldSize = (_a = headerSizeCacheRef.current.get(sectionIndex)) !== null && _a !== void 0 ? _a : getDefaultHeaderSize();
946
+ if (Math.abs(oldSize - measured) >= 1) {
947
+ headerSizeCacheRef.current.set(sectionIndex, measured);
948
+ if (isWeapp && props.useResizeObserver === true) {
949
+ scheduleWeappDynamicReflow();
950
+ }
951
+ else if (sizeCacheRafRef.current == null) {
952
+ sizeCacheRafRef.current = requestAnimationFrame(() => {
953
+ sizeCacheRafRef.current = null;
954
+ setSizeCacheVersion((v) => v + 1);
955
+ });
956
+ }
957
+ }
958
+ }
959
+ };
960
+ if (isH5) {
961
+ requestAnimationFrame(() => {
962
+ if (!headerRefsRef.current.has(sectionIndex))
963
+ return;
964
+ const rect = el.getBoundingClientRect();
965
+ measureAndUpdateHeader(isHorizontal ? rect.width : rect.height);
966
+ });
967
+ }
968
+ else if (isWeapp) {
969
+ Taro.nextTick(() => {
970
+ if (!headerRefsRef.current.has(sectionIndex))
971
+ return;
972
+ createSelectorQueryScoped(props.selectorQueryScope)
973
+ .select(`#${listId}-list-header-inner-${sectionIndex}`)
974
+ .boundingClientRect((rect) => {
975
+ if (rect) {
976
+ measureAndUpdateHeader(isHorizontal ? rect.width : rect.height);
977
+ }
978
+ })
979
+ .exec();
980
+ });
981
+ }
982
+ }
983
+ else {
984
+ const oldEl = headerRefsRef.current.get(sectionIndex);
985
+ if (oldEl) {
986
+ resizeObserver.unobserve(oldEl);
987
+ headerRefsRef.current.delete(sectionIndex);
988
+ }
989
+ }
990
+ } : undefined;
991
+ // 动态尺寸时:外层定位,内层撑开以便测量
992
+ const headerContentStyle = props.useResizeObserver
993
+ ? (isHorizontal
994
+ // weapp 下 max-content 兼容性不稳定,改用 inline-flex 收缩包裹以测得真实宽度
995
+ ? (isWeapp
996
+ ? { boxSizing: 'border-box', display: 'inline-flex', height: '100%' }
997
+ : { boxSizing: 'border-box', width: 'max-content', height: '100%' })
998
+ : { boxSizing: 'border-box', width: '100%' })
999
+ : {};
1000
+ if (props.useResizeObserver) {
1001
+ const headerInnerProps = {
1002
+ ref: headerRefCallback,
1003
+ style: headerContentStyle,
1004
+ 'data-index': String(-sectionIndex - 1), // 用于 unobserve 获取 index;与 observe(el, -sectionIndex-1) 对应
1005
+ };
1006
+ if (isWeapp) {
1007
+ headerInnerProps.id = `${listId}-list-header-inner-${sectionIndex}`;
1008
+ }
1009
+ nodes.push(React.createElement(View, {
1010
+ key: section.key + '-header',
1011
+ style: sectionHeaderStyle,
1012
+ }, React.createElement(View, headerInnerProps, section.header)));
1013
+ }
1014
+ else {
1015
+ nodes.push(React.createElement(View, {
1016
+ key: section.key + '-header',
1017
+ style: sectionHeaderStyle,
1018
+ }, section.header));
1019
+ }
302
1020
  offset += headerSize;
303
1021
  }
304
1022
  // item offsets
@@ -318,9 +1036,17 @@ const List = (props) => {
318
1036
  break;
319
1037
  }
320
1038
  }
1039
+ // 更新可见起始索引(用于 ScrollCorrection)
1040
+ if (s === startSection && startItem === 0) {
1041
+ visibleStartIndexRef.current = globalItemIndex;
1042
+ }
1043
+ else if (s === startSection) {
1044
+ visibleStartIndexRef.current = globalItemIndex + startItem;
1045
+ }
321
1046
  // 渲染可见item
322
1047
  for (let i = startItem; i <= endItem; i++) {
323
- // 内联样式替代className
1048
+ const currentGlobalIndex = globalItemIndex + i;
1049
+ const itemId = `list-item-${currentGlobalIndex}`;
324
1050
  const sectionItemStyle = Object.assign({ position: 'absolute', zIndex: 1, boxSizing: 'border-box', width: '100%', minHeight: '20px', overflow: 'hidden', lineHeight: 1 }, (isHorizontal
325
1051
  ? {
326
1052
  top: 0,
@@ -334,17 +1060,208 @@ const List = (props) => {
334
1060
  height: itemSizes[i],
335
1061
  marginBottom: space
336
1062
  }));
337
- nodes.push(React.createElement(View, {
1063
+ // ResizeObserver:绑定内层内容容器测量真实尺寸,尺寸变化时才 bump 版本
1064
+ const refCallback = (el) => {
1065
+ if (el) {
1066
+ const capturedIndex = currentGlobalIndex;
1067
+ itemRefsRef.current.set(capturedIndex, el);
1068
+ resizeObserver.observe(el, capturedIndex);
1069
+ // H5:使用 getBoundingClientRect 进行 fallback 测量
1070
+ // 小程序:使用 SelectorQuery 进行 fallback 测量(小程序没有 getBoundingClientRect)
1071
+ const measureAndUpdate = (measured) => {
1072
+ var _a;
1073
+ if (measured > 0) {
1074
+ const oldSize = sizeCache.getItemSize(capturedIndex);
1075
+ if (Math.abs(oldSize - measured) < 1)
1076
+ return;
1077
+ sizeCache.setItemSize(capturedIndex, measured);
1078
+ scrollCorrection.recordSizeChange(capturedIndex, oldSize, measured);
1079
+ if (isWeapp && props.useResizeObserver === true) {
1080
+ scheduleWeappDynamicReflow();
1081
+ }
1082
+ else if (sizeCacheRafRef.current == null) {
1083
+ sizeCacheRafRef.current = requestAnimationFrame(() => {
1084
+ sizeCacheRafRef.current = null;
1085
+ setSizeCacheVersion((v) => v + 1);
1086
+ });
1087
+ }
1088
+ // 小程序:延迟 onItemSizeChange,避免父组件重渲染导致 List remount
1089
+ if (isWeapp) {
1090
+ weappDeferItemSizeChange(capturedIndex, measured, props.onItemSizeChange);
1091
+ }
1092
+ else {
1093
+ (_a = props.onItemSizeChange) === null || _a === void 0 ? void 0 : _a.call(props, capturedIndex, measured);
1094
+ }
1095
+ }
1096
+ };
1097
+ if (isH5) {
1098
+ requestAnimationFrame(() => {
1099
+ if (!itemRefsRef.current.has(capturedIndex))
1100
+ return;
1101
+ const rect = el.getBoundingClientRect();
1102
+ measureAndUpdate(isHorizontal ? rect.width : rect.height);
1103
+ });
1104
+ }
1105
+ else if (isWeapp) {
1106
+ // 小程序端:使用 SelectorQuery 测量内层内容容器的实际尺寸
1107
+ // 注意:必须选 inner 容器(无固定高度,由内容撑开),不能选 outer(有虚拟列表设置的固定高度)
1108
+ // 已测量过的 item 跳过,避免 refCallback 重复触发导致无限循环
1109
+ // 滚动期间跳过 SelectorQuery(不加入 weappMeasuredItemsRef),避免异步查询干扰 scroll-view
1110
+ // SelectorQuery 是只读查询,滚动期间执行安全
1111
+ if (shouldMeasureWeappItem(capturedIndex, weappMeasuredItemsRef.current)) {
1112
+ weappMeasuredItemsRef.current.add(capturedIndex);
1113
+ // 使用 Taro.nextTick 代替 setTimeout(50):等待下一帧渲染完成后再测量
1114
+ // 比固定延时更快(减少"预估→实测"闪烁)且更可靠(保证 DOM 已更新)
1115
+ Taro.nextTick(() => {
1116
+ if (!itemRefsRef.current.has(capturedIndex))
1117
+ return;
1118
+ createSelectorQueryScoped(props.selectorQueryScope)
1119
+ // 页面上可能同时存在多个 List,inner id 必须带 listId 前缀避免跨列表误命中
1120
+ .select(`#${listId}-list-item-inner-${capturedIndex}`)
1121
+ .boundingClientRect((rect) => {
1122
+ if (rect) {
1123
+ measureAndUpdate(isHorizontal ? rect.width : rect.height);
1124
+ }
1125
+ })
1126
+ .exec();
1127
+ });
1128
+ }
1129
+ }
1130
+ }
1131
+ else {
1132
+ const oldEl = itemRefsRef.current.get(currentGlobalIndex);
1133
+ if (oldEl) {
1134
+ resizeObserver.unobserve(oldEl);
1135
+ itemRefsRef.current.delete(currentGlobalIndex);
1136
+ }
1137
+ }
1138
+ };
1139
+ // 动态尺寸时:外层负责定位,内层由内容撑开以便测量真实尺寸。
1140
+ // 纵向:内层 width:100% 无 height,由内容撑开,测到的是内容高度。
1141
+ // 横向:width:max-content + height:100%:
1142
+ // - width:max-content 便于测量真实宽度;
1143
+ // - height:100% 让测量层与外层 slot 条带高度一致,避免「外层条带 180px、内层 wrapper 只有内容高度」导致的内外高度差。
1144
+ const contentWrapperStyle = props.useResizeObserver
1145
+ ? (isHorizontal
1146
+ // weapp 下 max-content 兼容性不稳定,改用 inline-flex 收缩包裹以测得真实宽度
1147
+ ? (isWeapp
1148
+ ? { boxSizing: 'border-box', display: 'inline-flex', height: '100%' }
1149
+ : { boxSizing: 'border-box', width: 'max-content', height: '100%' })
1150
+ : { boxSizing: 'border-box', width: '100%' })
1151
+ : {};
1152
+ const outerItemProps = {
338
1153
  key: section.key + '-item-' + i,
1154
+ id: itemId,
339
1155
  style: sectionItemStyle,
340
- }, section.items[i]));
1156
+ };
1157
+ if (props.useResizeObserver) {
1158
+ const innerProps = {
1159
+ ref: refCallback,
1160
+ style: contentWrapperStyle,
1161
+ };
1162
+ // 小程序端需要 id 用于 SelectorQuery 测量内容真实尺寸;H5 端不需要(用 getBoundingClientRect)
1163
+ if (isWeapp) {
1164
+ // 页面内多 List 并存时避免 id 冲突(例如 demo 同页含多个 List)
1165
+ innerProps.id = `${listId}-list-item-inner-${currentGlobalIndex}`;
1166
+ }
1167
+ innerProps['data-index'] = String(currentGlobalIndex);
1168
+ const itemNode = React.createElement(View, outerItemProps, React.createElement(View, innerProps, section.items[i]));
1169
+ // 任务 4.1:当 List 暴露 scrollRef 时,为每个 item 提供 Context,供内层 WaterFlow 使用
1170
+ const itemWithContext = scrollRefProp && !useScrollElementMode
1171
+ ? React.createElement(ScrollElementContextOrFallback.Provider, {
1172
+ key: outerItemProps.key,
1173
+ value: {
1174
+ scrollRef: scrollRefProp,
1175
+ containerHeight: containerLength,
1176
+ startOffset: offset + itemOffsets[i],
1177
+ reportNestedHeightChange: props.useResizeObserver
1178
+ ? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
1179
+ : undefined,
1180
+ },
1181
+ }, itemNode)
1182
+ : itemNode;
1183
+ nodes.push(itemWithContext);
1184
+ }
1185
+ else {
1186
+ const itemNode = React.createElement(View, outerItemProps, section.items[i]);
1187
+ const itemWithContext = scrollRefProp && !useScrollElementMode
1188
+ ? React.createElement(ScrollElementContextOrFallback.Provider, {
1189
+ key: outerItemProps.key,
1190
+ value: {
1191
+ scrollRef: scrollRefProp,
1192
+ containerHeight: containerLength,
1193
+ startOffset: offset + itemOffsets[i],
1194
+ reportNestedHeightChange: props.useResizeObserver
1195
+ ? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
1196
+ : undefined,
1197
+ },
1198
+ }, itemNode)
1199
+ : itemNode;
1200
+ nodes.push(itemWithContext);
1201
+ }
341
1202
  }
1203
+ globalItemIndex += section.items.length;
342
1204
  offset += itemOffsets[itemOffsets.length - 1];
343
1205
  }
344
1206
  return nodes;
345
1207
  };
346
- return (jsx(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { children: jsxs(View, { style: isHorizontal ? { width: totalLength, position: 'relative', height: '100%' } : { height: totalLength, position: 'relative', width: '100%' }, children: [stickyHeaderNode, renderSections()] }) })));
1208
+ // 渲染 NoMore 内容
1209
+ const renderNoMoreContent = () => {
1210
+ if (!noMoreConfig || !noMoreConfig.visible)
1211
+ return null;
1212
+ const noMoreHeightValue = noMoreConfig.height || 60;
1213
+ const listContentEnd = sectionOffsets[sectionOffsets.length - 1];
1214
+ const defaultStyle = Object.assign(Object.assign(Object.assign({ position: 'absolute' }, (isHorizontal
1215
+ ? { left: listContentEnd, top: 0, width: noMoreHeightValue, height: '100%' }
1216
+ : { top: listContentEnd, left: 0, width: '100%', height: noMoreHeightValue })), { display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', color: '#999', fontSize: '14px', boxSizing: 'border-box' }), noMoreConfig.style);
1217
+ return (jsx(View, { style: defaultStyle, children: noMoreConfig.children || noMoreConfig.text || '没有更多了' }));
1218
+ };
1219
+ // 空列表场景:仅显示 NoMore
1220
+ if (sections.length === 0 && (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible)) {
1221
+ return (jsx(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { children: jsx(View, { style: Object.assign({ minHeight: containerLength, display: 'flex', alignItems: 'center', justifyContent: 'center' }, containerStyle), children: renderNoMoreContent() }) })));
1222
+ }
1223
+ // 可滚区域总尺寸
1224
+ // H5 refresher 用负 translateY 隐藏,有上方混排时可能延伸到 sibling 区域;overflow:hidden 裁剪避免重叠
1225
+ const needsRefresherClip = refresherHeightForH5 > 0;
1226
+ const contentWrapperStyle = isHorizontal
1227
+ ? Object.assign({ width: totalLength, position: 'relative', height: '100%' }, (needsRefresherClip && { overflow: 'hidden' })) : Object.assign({ height: totalLength, position: 'relative', width: '100%' }, (needsRefresherClip && { overflow: 'hidden' }));
1228
+ const listWrapperStyle = isHorizontal
1229
+ ? { width: listContentLength, position: 'relative', height: '100%' }
1230
+ : { height: listContentLength, position: 'relative', width: '100%' };
1231
+ const pullTranslate = refresherHeightForH5 > 0 && h5RefresherProps.pullDistance !== 0
1232
+ ? { transform: `translateY(${h5RefresherProps.pullDistance}px)` }
1233
+ : {};
1234
+ const h5RefresherTranslateY = -refresherHeightForH5 + h5RefresherProps.pullDistance;
1235
+ // 内容区域渲染
1236
+ const renderContentArea = () => {
1237
+ var _a, _b;
1238
+ return (jsx(View, { style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
1239
+ position: 'absolute',
1240
+ top: 0,
1241
+ left: 0,
1242
+ right: 0,
1243
+ height: refresherHeightForH5,
1244
+ zIndex: 0,
1245
+ transform: `translateY(${h5RefresherTranslateY}px)`,
1246
+ }, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_b = (_a = style === null || style === void 0 ? void 0 : style.background) !== null && _a !== void 0 ? _a : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _b !== void 0 ? _b : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [!supportsNativeRefresher && renderRefresherContent(), stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
1247
+ };
1248
+ // useScrollElementMode 或 needAutoFind&&pending:渲染 View 以便监听外部滚动或 probe 阶段查找滚动父节点
1249
+ const renderView = useScrollElementMode || (!!needAutoFind && autoFindStatus === 'pending');
1250
+ if (renderView) {
1251
+ // 任务 2.4:恢复 refresher DOM 结构(refresher 层 + listWrapperStyle)
1252
+ return (jsx(View, { ref: contentWrapperRef, id: contentId, style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
1253
+ position: 'absolute',
1254
+ top: 0,
1255
+ left: 0,
1256
+ right: 0,
1257
+ height: refresherHeightForH5,
1258
+ zIndex: 0,
1259
+ transform: `translateY(${h5RefresherTranslateY}px)`,
1260
+ }, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_b = (_a = style === null || style === void 0 ? void 0 : style.background) !== null && _a !== void 0 ? _a : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _b !== void 0 ? _b : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
1261
+ }
1262
+ return (jsxs(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { id: listId, children: [supportsNativeRefresher && renderRefresherContent(), renderContentArea()] })));
347
1263
  };
1264
+ const List = React.forwardRef(InnerList);
348
1265
 
349
- export { List, ListItem, StickyHeader, StickySection, accumulate, List as default, isShaking };
1266
+ export { List, ListItem, ScrollElementContextOrFallback as ListScrollElementContext, NoMore, StickyHeader, StickySection, accumulate, List as default, isShaking };
350
1267
  //# sourceMappingURL=index.js.map