Skip to content

分时函数 ​

试想这么一个场景,页面上一次性渲染100000个元素,没有任何性能优化操作前,页面渲染一定会卡顿,导致用户体验不佳。

我们可以使用 分时函数 来解决这个问题。

TIP

需要注意的是:分时函数只解决渲染卡顿时间,并不能缩短渲染时间


分时函数原理 ​

分数函数的原理是将一个大的任务分成多个小的任务,然后在每个小任务之间让浏览器有机会去渲染页面。这样可以避免一次性渲染大量元素导致的卡顿现象。

基于requestIdleCallback的基础实现 ​

我们可以实现一个分时函数的工具函数,来帮助我们实现这个功能。下面是一个简单的实现:

ts
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的函数来替代它。那么我们需要解决两个问题:

  1. 如何分割执行块?
  2. 如何判断还有执行时间吗?

我们逐一解决,我们需要一个类似于requestIdleCallback的函数,用来替代它,我们把它叫做chunkSplitor表示块的分割器,它的用法 和requestIdleCallback类似,都是传入一个函数task和一个时间参数,由chunkSplitor决定传递的函数什么时候去执行。 下面给出chunkSplitor的简单实现:

js
const chunkSplitor = (task) => {
  setTimeout(() => {
    // 这里是一个空闲时间
    task();
  }, 20);
}

通过 setTimeout 我们解决了如何分割执行块问题 接下来我们需要解决如何判断还有执行时间的问题 当我们使用 chunkSplitor 时,怎么判断还有没有执行时间呢?我们需要增加hasTime参数决定 并且我们需要在代码执行出处理时间判断逻辑,逻辑如下:

js
function performChunk(datas, consumer){
 ... 
  function _run(){
    chunkSplitor((hasTime)=>{
      cnst start = Date.now();
      while(hasTime(Date.now() - start) > 0 && i < datas.length){
        ...
      }
    })
  }
}

任务开始前记录结果一个时间,然后循环的时候调用hasTime这个函数,将两个时间相减,有了这个参数传入到chunkSplitor中

js
const chunkSplitor = (task) => {
  setTimeout(() => {
    // 这里是一个空闲时间
    task((time) => time < 16);
  }, 20);
}

自定义基础 ​

ts
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 ​

ts
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;
    },
  };
}

使用示例 ​

ts
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();

上次更新于: