Skip to content

Commit 49188de

Browse files
jarmywangBoomkaa
andauthored
fix(picker): Optimize performance and avoid frame drops (#4120)
* fix(picker): 优化性能,避免掉帧 * fix(picker): 增加大量数据测试方法 * fix: 减少touchMove期间的setData数据量 * fix(picker): 删除冗余变量和逻辑 * fix(picker): 解决平铺模式未响应value变化 --------- Co-authored-by: Boomkaa <[email protected]>
1 parent b8f40a5 commit 49188de

File tree

3 files changed

+116
-113
lines changed

3 files changed

+116
-113
lines changed

packages/components/picker-item/picker-item.ts

Lines changed: 57 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { PickerItemOption } from './type';
66
const { prefix } = config;
77
const name = `${prefix}-picker-item`;
88

9-
// 动画持续时间
10-
const ANIMATION_DURATION = 1000;
9+
// 动画持续时间(优化:根据滑动距离动态计算,基础时长降低)
10+
const ANIMATION_DURATION_BASE = 300; // 基础动画时长
11+
const ANIMATION_DURATION_MAX = 600; // 最大动画时长
1112
// 和上一次move事件间隔小于INERTIA_TIME
1213
const INERTIA_TIME = 300;
1314
// 且距离大于`MOMENTUM_DISTANCE`时,执行惯性滚动
@@ -93,18 +94,10 @@ export default class PickerItem extends SuperComponent {
9394
this.StartY = 0;
9495
this.StartOffset = 0;
9596
this.startTime = 0;
96-
this._moveTimer = null;
9797
this._animationTimer = null; // 动画期间更新虚拟滚动的定时器
98-
this._lastOffset = 0; // 上一次的偏移量(用于计算滑动速度)
99-
this._lastMoveTime = 0; // 上一次移动的时间
100-
this._scrollDirection = 0; // 滑动方向:1向下,-1向上,0静止
10198
},
10299
detached() {
103100
// 清理定时器,防止内存泄漏
104-
if (this._moveTimer) {
105-
clearTimeout(this._moveTimer);
106-
this._moveTimer = null;
107-
}
108101
if (this._animationTimer) {
109102
clearInterval(this._animationTimer);
110103
this._animationTimer = null;
@@ -136,61 +129,19 @@ export default class PickerItem extends SuperComponent {
136129

137130
onTouchMove(event) {
138131
const { StartY, StartOffset } = this;
139-
const { itemHeight, enableVirtualScroll } = this.data;
140-
const currentTime = Date.now();
132+
const { itemHeight } = this.data;
141133

142134
// 偏移增量
143135
const deltaY = event.touches[0].clientY - StartY;
144-
const newOffset = range(StartOffset + deltaY, -(this.getCount() * itemHeight), 0);
145-
146-
// 计算滑动速度和方向
147-
const offsetDelta = newOffset - this._lastOffset;
148-
const timeDelta = currentTime - this._lastMoveTime || 16;
149-
const scrollSpeed = Math.abs(offsetDelta / timeDelta) * 16; // 转换为 px/frame
150-
151-
// 计算滑动方向(避免嵌套三元表达式)
152-
if (offsetDelta > 0) {
153-
this._scrollDirection = 1; // 向下滑动
154-
} else if (offsetDelta < 0) {
155-
this._scrollDirection = -1; // 向上滑动
156-
} else {
157-
this._scrollDirection = 0; // 静止
158-
}
159-
160-
// 判断是否为快速滑动
161-
const isFastScroll = scrollSpeed > VIRTUAL_SCROLL_CONFIG.FAST_SCROLL_THRESHOLD;
162-
163-
// 优化节流策略:快速滑动时立即更新,慢速滑动时节流
164-
if (!this._moveTimer || isFastScroll) {
165-
if (this._moveTimer) {
166-
clearTimeout(this._moveTimer);
167-
this._moveTimer = null;
168-
}
136+
const newOffset = range(StartOffset + deltaY, -(this.getCount() - 1) * itemHeight, 0);
169137

170-
this.setData({ offset: newOffset });
171-
172-
// 虚拟滚动:更新可见范围(快速滑动时使用更大的缓冲区)
173-
if (enableVirtualScroll) {
174-
this.updateVisibleOptions(newOffset, isFastScroll);
175-
}
176-
177-
this._moveTimer = setTimeout(() => {
178-
this._moveTimer = null;
179-
}, VIRTUAL_SCROLL_CONFIG.THROTTLE_TIME);
180-
}
181-
182-
// 记录当前状态
183-
this._lastOffset = newOffset;
184-
this._lastMoveTime = currentTime;
138+
// touchMove 期间只更新 offset,不更新虚拟滚动数据
139+
// 虚拟滚动数据在 touchEnd 时统一更新,避免频繁 setData 导致掉帧
140+
this.setData({ offset: newOffset });
185141
},
186142

187143
onTouchEnd(event) {
188-
if (this._moveTimer) {
189-
clearTimeout(this._moveTimer);
190-
this._moveTimer = null;
191-
}
192-
193-
const { offset, itemHeight } = this.data;
144+
const { offset, itemHeight, enableVirtualScroll, formatOptions } = this.data;
194145
const { startTime } = this;
195146
if (offset === this.StartOffset) {
196147
return;
@@ -207,63 +158,64 @@ export default class PickerItem extends SuperComponent {
207158
// 调整偏移量
208159
const newOffset = range(offset + distance, -this.getCount() * itemHeight, 0);
209160
const index = range(Math.round(-newOffset / itemHeight), 0, this.getCount() - 1);
161+
const finalOffset = -index * itemHeight;
162+
163+
// 动态计算动画时长:根据滑动距离调整,距离越大时长越长,但有上限
164+
const scrollDistance = Math.abs(finalOffset - offset);
165+
const scrollItems = scrollDistance / itemHeight;
166+
const animationDuration = Math.min(
167+
ANIMATION_DURATION_MAX,
168+
ANIMATION_DURATION_BASE + scrollItems * 30, // 每滑动一个选项增加30ms
169+
);
210170

211-
// 判断是否为快速惯性滚动
171+
// 判断是否为快速惯性滚动(用于决定缓冲区大小)
212172
const isFastInertia = Math.abs(distance) > itemHeight * 3;
213-
214-
// 立即更新虚拟滚动视图(修复惯性滚动后空白问题,快速滚动时使用更大缓冲区)
215-
if (this.data.enableVirtualScroll) {
216-
this.updateVisibleOptions(-index * itemHeight, isFastInertia);
217-
}
173+
// 根据是否快速惯性滚动选择缓冲区大小
174+
const bufferCount = isFastInertia ? VIRTUAL_SCROLL_CONFIG.FAST_SCROLL_BUFFER : VIRTUAL_SCROLL_CONFIG.BUFFER_COUNT;
218175

219176
// 清除之前的动画更新定时器
220177
if (this._animationTimer) {
221178
clearInterval(this._animationTimer);
222179
this._animationTimer = null;
223180
}
224181

225-
// 在动画执行期间定期更新虚拟滚动视图(确保动画过程流畅)
226-
if (this.data.enableVirtualScroll && Math.abs(distance) > 0) {
227-
const startOffset = offset;
228-
const endOffset = -index * itemHeight;
229-
const startTime = Date.now();
182+
// 性能优化:合并 setData 调用,一次性更新所有状态
183+
const updateData: any = {
184+
offset: finalOffset,
185+
duration: animationDuration,
186+
curIndex: index,
187+
};
230188

231-
this._animationTimer = setInterval(() => {
232-
const elapsed = Date.now() - startTime;
233-
const progress = Math.min(elapsed / ANIMATION_DURATION, 1);
189+
// 虚拟滚动:预先计算覆盖动画全程的可见范围,避免动画期间频繁更新
190+
if (enableVirtualScroll) {
191+
// 计算当前位置和目标位置的索引范围
192+
const currentIndex = Math.floor(Math.abs(offset) / itemHeight);
193+
const targetIndex = index;
234194

235-
// 使用缓动函数计算当前偏移量
236-
const easeOutCubic = 1 - (1 - progress) ** 3;
237-
const currentOffset = startOffset + (endOffset - startOffset) * easeOutCubic;
195+
// 计算覆盖动画全程的可见范围(从当前位置到目标位置)
196+
const minIndex = Math.min(currentIndex, targetIndex);
197+
const maxIndex = Math.max(currentIndex, targetIndex);
238198

239-
// 快速惯性滚动时使用更大的缓冲区
240-
this.updateVisibleOptions(currentOffset, isFastInertia);
199+
// 使用缓冲区扩展范围,确保动画过程中不会出现空白
200+
const animationStartIndex = Math.max(0, minIndex - bufferCount);
201+
const animationEndIndex = Math.min(formatOptions.length, maxIndex + this.data.visibleItemCount + bufferCount);
241202

242-
if (progress >= 1) {
243-
clearInterval(this._animationTimer);
244-
this._animationTimer = null;
245-
}
246-
}, 16); // 约60fps
203+
updateData.visibleOptions = formatOptions.slice(animationStartIndex, animationEndIndex);
204+
updateData.virtualStartIndex = animationStartIndex;
205+
updateData.virtualOffsetY = animationStartIndex * itemHeight;
247206
}
248207

249-
this.setData(
250-
{
251-
offset: -index * itemHeight,
252-
duration: ANIMATION_DURATION,
253-
curIndex: index,
254-
},
255-
() => {
256-
// 动画结束后清除定时器并最终更新虚拟滚动视图
257-
if (this._animationTimer) {
258-
clearInterval(this._animationTimer);
259-
this._animationTimer = null;
260-
}
261-
if (this.data.enableVirtualScroll) {
262-
// 动画结束后使用正常缓冲区(不再是快速滚动状态)
263-
this.updateVisibleOptions(-index * itemHeight, false);
264-
}
265-
},
266-
);
208+
this.setData(updateData, () => {
209+
// 动画结束后,精确更新虚拟滚动视图到最终位置
210+
if (enableVirtualScroll) {
211+
const visibleRange = this.computeVirtualRange(finalOffset, formatOptions.length, itemHeight, false);
212+
this.setData({
213+
visibleOptions: formatOptions.slice(visibleRange.startIndex, visibleRange.endIndex),
214+
virtualStartIndex: visibleRange.startIndex,
215+
virtualOffsetY: visibleRange.startIndex * itemHeight,
216+
});
217+
}
218+
});
267219

268220
if (index === this._selectedIndex) return;
269221
this.updateSelected(index, true);
@@ -355,21 +307,15 @@ export default class PickerItem extends SuperComponent {
355307
const { visibleItemCount } = this.data;
356308

357309
// 根据滑动速度动态调整缓冲区大小
358-
const dynamicBuffer = isFastScroll ? FAST_SCROLL_BUFFER : BUFFER_COUNT;
359-
360-
// 根据滑动方向调整缓冲区分配
361-
// 向上滑动(_scrollDirection = -1):增加顶部缓冲区
362-
// 向下滑动(_scrollDirection = 1):增加底部缓冲区
363-
const topBuffer = this._scrollDirection === -1 ? dynamicBuffer + 2 : dynamicBuffer;
364-
const bottomBuffer = this._scrollDirection === 1 ? dynamicBuffer + 2 : dynamicBuffer;
310+
const bufferCount = isFastScroll ? FAST_SCROLL_BUFFER : BUFFER_COUNT;
365311

366312
// 计算当前可见区域的中心索引
367313
const centerIndex = Math.floor(scrollTop / itemHeight);
368314

369-
// 计算起始索引(减去顶部缓冲区
370-
const startIndex = Math.max(0, centerIndex - topBuffer);
371-
// 计算结束索引(加上可见数量和底部缓冲区
372-
const endIndex = Math.min(totalCount, centerIndex + visibleItemCount + bottomBuffer);
315+
// 计算起始索引(减去缓冲区
316+
const startIndex = Math.max(0, centerIndex - bufferCount);
317+
// 计算结束索引(加上可见数量和缓冲区
318+
const endIndex = Math.min(totalCount, centerIndex + visibleItemCount + bufferCount);
373319

374320
return { startIndex, endIndex };
375321
},

packages/components/picker/_example/area/index.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,61 @@ const areaList = {
7777
},
7878
};
7979

80+
// 使用这份数据可以模拟大量数据场景
81+
// // 生成广东省1000个城市
82+
// const generateCities = () => {
83+
// const cities = {
84+
// 110100: '北京市',
85+
// };
86+
//
87+
// // 生成广东省1000个城市
88+
// for (let i = 1; i <= 1000; i++) {
89+
// const cityCode = 440000 + i * 100;
90+
// cities[cityCode] = `广东城市${i}`;
91+
// }
92+
//
93+
// return cities;
94+
// };
95+
//
96+
// // 生成广州市10000个地区
97+
// const generateCounties = () => {
98+
// const counties = {
99+
// 110101: '东城区',
100+
// 110102: '西城区',
101+
// 110105: '朝阳区',
102+
// 110106: '丰台区',
103+
// 110107: '石景山区',
104+
// 110108: '海淀区',
105+
// 110109: '门头沟区',
106+
// 110111: '房山区',
107+
// 110112: '通州区',
108+
// 110113: '顺义区',
109+
// 110114: '昌平区',
110+
// 110115: '大兴区',
111+
// 110116: '怀柔区',
112+
// 110117: '平谷区',
113+
// 110118: '密云区',
114+
// 110119: '延庆区',
115+
// };
116+
//
117+
// // 生成广州市(440100)10000个地区
118+
// for (let i = 1; i <= 10000; i++) {
119+
// const countyCode = 44010000 + i;
120+
// counties[countyCode] = `广州地区${i}`;
121+
// }
122+
//
123+
// return counties;
124+
// };
125+
//
126+
// const areaList = {
127+
// provinces: {
128+
// 110000: '北京市',
129+
// 440000: '广东省',
130+
// },
131+
// cities: generateCities(),
132+
// counties: generateCounties(),
133+
// };
134+
80135
const getOptions = (obj, filter) => {
81136
const res = Object.keys(obj).map((key) => ({ value: key, label: obj[key] }));
82137

packages/components/picker/picker.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ export default class Picker extends SuperComponent {
3131

3232
observers = {
3333
'value, visible'(value, visible) {
34-
// 只在打开弹窗或value变化时更新,关闭时不更新避免滚动
35-
if (visible) {
34+
const { usePopup } = this.properties;
35+
// 平铺模式(usePopup=false):始终响应 value 变化
36+
// 弹窗模式(usePopup=true):只在打开弹窗时更新,关闭时不更新避免回弹
37+
if (!usePopup || visible) {
3638
this.updateChildren();
3739
this.updateIndicatorPosition();
3840
}

0 commit comments

Comments
 (0)