分时函数
试想这么一个场景,页面上一次性渲染100000个元素,没有任何性能优化操作前,页面渲染一定会卡顿,导致用户体验不佳。
我们可以使用 分时函数 来解决这个问题。
TIP
需要注意的是:分时函数只解决渲染卡顿时间,并不能缩短渲染时间
分时函数原理
分数函数的原理是将一个大的任务分成多个小的任务,然后在每个小任务之间让浏览器有机会去渲染页面。这样可以避免一次性渲染大量元素导致的卡顿现象。
基于requestIdleCallback的基础实现
我们可以实现一个分时函数的工具函数,来帮助我们实现这个功能。下面是一个简单的实现:
function performChunk(datas: number | any[], consumer: (data: any) => void) {
// 参数归一化处理
if (typeof datas === 'number') {
datas = Array.from({ length: datas }, (_, i) => i);
}
if (datas.length === 0) return;
let i = 0; // 目前应该取出的任务小标
// 执行一些任务
function _run() {
if (i === datas.length) return;
// 取出当前任务
requestIdleCallback(idle => {
// 这一帧还有空闲时间
while (idle.timeRemaining() > 0 && i < datas.length) {
// 执行当前任务
consumer(datas[i]);
i++;
}
})
}
}上述实现是一个简单的分时函数实现,使用了requestIdleCallback来判断浏览器是否有空闲时间来执行任务。requestIdleCallback是一个浏览器提供的API,不一定所有浏览器都支持,所以我们需要考虑兼容性问题。因此我们需要实现一个类似于requestIdleCallback的函数来替代它。那么我们需要解决两个问题:
- 如何分割执行块?
- 如何判断还有执行时间吗?
我们逐一解决,我们需要一个类似于requestIdleCallback的函数,用来替代它,我们把它叫做chunkSplitor表示块的分割器,它的用法 和requestIdleCallback类似,都是传入一个函数task和一个时间参数,由chunkSplitor决定传递的函数什么时候去执行。 下面给出chunkSplitor的简单实现:
const chunkSplitor = (task) => {
setTimeout(() => {
// 这里是一个空闲时间
task();
}, 20);
}通过 setTimeout 我们解决了如何分割执行块问题 接下来我们需要解决如何判断还有执行时间的问题 当我们使用 chunkSplitor 时,怎么判断还有没有执行时间呢?我们需要增加hasTime参数决定 并且我们需要在代码执行出处理时间判断逻辑,逻辑如下:
function performChunk(datas, consumer){
...
function _run(){
chunkSplitor((hasTime)=>{
cnst start = Date.now();
while(hasTime(Date.now() - start) > 0 && i < datas.length){
...
}
})
}
}任务开始前记录结果一个时间,然后循环的时候调用hasTime这个函数,将两个时间相减,有了这个参数传入到chunkSplitor中
const chunkSplitor = (task) => {
setTimeout(() => {
// 这里是一个空闲时间
task((time) => time < 16);
}, 20);
}自定义基础
const chunkSplitor = (task: (hasTime: (elapsed: number) => boolean) => void) => {
setTimeout(() => {
// 这里是一个空闲时间
task((elapsed) => elapsed < 16);
}, 20);
}
function performChunk(datas: number | any[], consumer: (data: any) => void) {
// 参数归一化处理
if (typeof datas === 'number') {
datas = Array.from({ length: datas }, (_, i) => i);
}
if (datas.length === 0) return;
let i = 0; // 目前应该取出的任务小标
// 执行一些任务
function _run() {
if (i === datas.length) return;
// 取出当前任务
chunkSplitor((hasTime) => {
const start = Date.now();
while (hasTime(Date.now() - start) && i < datas.length) {
// 执行当前任务
consumer(datas[i]);
i++;
}
});
}
_run(); // Ensure the function is called to start processing
}vue3 hook
type ChunkSchedulerOptions<T> = {
data: number | T[];
consumer: (item: T, index: number) => void;
chunkSize?: number; // 每批处理的最大数量
onChunk?: (startIndex: number, endIndex: number) => void;
onDone?: () => void;
};
export function useChunkScheduler<T = any>(options: ChunkSchedulerOptions<T>) {
const {
data,
consumer,
chunkSize = 100,
onChunk,
onDone,
} = options;
const dataset = typeof data === 'number' ? Array.from({ length: data }, (_, i) => i as T) : data;
let i = 0;
let stopped = false;
const run = (deadline?: IdleDeadline) => {
if (i >= dataset.length || stopped) {
if (i >= dataset.length && onDone) onDone();
return;
}
const start = i;
let count = 0;
const canContinue = () =>
!deadline || deadline.timeRemaining() > 0 || deadline.didTimeout;
while (i < dataset.length && count < chunkSize && canContinue()) {
consumer(dataset[i], i);
i++;
count++;
}
onChunk?.(start, i - 1);
if (i < dataset.length && !stopped) {
scheduleNext();
} else if (i >= dataset.length && onDone) {
onDone();
}
};
const scheduleNext = () => {
if (typeof requestIdleCallback !== 'undefined') {
requestIdleCallback(run);
} else {
setTimeout(() => run(), 16);
}
};
return {
start() {
stopped = false;
scheduleNext();
},
stop() {
stopped = true;
},
reset() {
i = 0;
stopped = false;
},
isDone() {
return i >= dataset.length;
},
progress() {
return i / dataset.length;
},
};
}使用示例
const scheduler = useChunkScheduler({
data: 5000,
chunkSize: 100,
consumer: (index) => {
const div = document.createElement('div');
div.textContent = `Item ${index}`;
div.style.padding = '4px';
document.body.appendChild(div);
},
onChunk: (start, end) => {
console.log(`渲染第 ${start} ~ ${end} 条`);
},
onDone: () => {
console.log('✅ 渲染完成');
}
});
scheduler.start();