时间切片编程 timeslice

最近在做思考总结,项目原来在做智能合同的项目关于文档比对导致页面卡断问题,项目组同事给了我一个切片编程的方案很棒,在此做个总结!

需求

源文档和修改后的文档,差异点存在近 5000+ 的差异点需要在两个文档上进行颜色标注,整个标注过程都是在进行 DOM 操作,做完后存在严重的页面卡死问题,甚至等待 5~8分钟才标注完成,甚至卡死!这个问题亟待解决优化!

分析

在事件循环机制中主线程需要全部执行完才会进行页面渲染,5000+ 的 DOM 操作严重阻塞了主线程,所以是不是可以给主线程让出空间,将dom操作任务放进宏任务队列,在下一次渲染之后继续执行DOM操作呢?

思路

1、执行主线程,DOM操作(先进行20次dom操作,剩余的任务放进setTimeout宏任务)
2、页面渲染,显示差异对比的颜色标注(上一次dom操作的结果)
3、执行宏任务中 20个 DOM 操作任务
4、页面渲染
5、重复 3、4

最后是结了这个方案,5000+ 的dom操作还是蛮顺滑的!最后我们讲讲时间切片思想

什么是时间切片

时间切片的核心思想是:如果任务不能在50毫秒内执行完,那么为了不阻塞主线程,这个任务应该让出主线程的控制权,使浏览器可以处理其他任务。让出控制权意味着停止执行当前任务,让浏览器去执行其他任务,随后再回来继续执行没有执行完的任务。

所以时间切片的目的是不阻塞主线程,而实现目的的技术手段是将一个长任务拆分成很多个不超过50ms的小任务分散在宏任务队列中执行。

在这里插入图片描述
上图可以看到主线程中有一个长任务,这个任务会阻塞主线程。使用时间切片将它切割成很多个小任务后,如下图所示。

在这里插入图片描述
可以看到现在的主线程有很多密密麻麻的小任务,我们将它放大后如下图所示。
在这里插入图片描述
可以看到每个小任务中间是有空隙的,代表着任务执行了一小段时间后,将让出主线程的控制权,让浏览器执行其他的任务。

使用时间切片的缺点是,任务运行的总时间变长了,这是因为它每处理完一个小任务后,主线程会空闲出来,并且在下一个小任务开始处理之前有一小段延迟。
但是为了避免卡死浏览器,这种取舍是很有必要的。

如何使用时间切片

目前发现有两种解决方案,不过起主体还是通过让出主线程的控制权
1、setTimeout
2、requestAnimationFrame
3、setTimeout + Generator

方案一:setTimeout

function request() {
  let result = [];
  for (let i = 0; i < 100000; i++) {
    result.push({
      index: i,
      text: ~~(Math.random() * 100000000),
    });
  }
  return Promise.resolve(result);
}

let ul = document.querySelector("#app");

request().then((res) => {
  let total = res.length;
  let once = 20;
  let page = total / once;
  let curIndex = 0;

  function loop(curTotal, curIndex) {

    let pageCount = Math.min(curTotal , once);

    setTimeout(function () {
      let fragement = document.createDocumentFragment();
      for (let i = 0; i < pageCount; i++) {
        let li = document.createElement("li");
        li.innerText =
          "index: " + res[curIndex + i].index + "; text: " + res[curIndex + i].text;
        fragement.appendChild(li);
      }

      ul.appendChild(fragement);
      loop(curTotal - pageCount, curIndex + pageCount);
    });
  }

  loop(total, curIndex);
}

简单聊一下 setTimeout 和闪屏现象
  • setTimeout的执行时间并不是确定的。在JS中,setTimeout任务被放进事件队列中,只有主线程执行完才会去检查事件队列中的任务是否需要执行,因此setTimeout的实际执行时间可能会比其设定的时间晚一些。
  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的刷新频率可能会不同,而setTimeout只能设置一个固定时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致。
在setTimeout中对dom进行操作,必须要等到屏幕下次绘制时才能更新到屏幕上,如果两者步调不一致,就可能导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素,从而导致丢帧现象。

方案二:requestAnimationFrame

与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。

如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。

function request() {
  let result = [];
  for (let i = 0; i < 100000; i++) {
    result.push({
      index: i,
      text: ~~(Math.random() * 100000000),
    });
  }
  return Promise.resolve(result);
}

let ul = document.querySelector("#app");

request().then((res) => {
  let total = res.length;
  let once = 20;
  let page = total / once;
  let curIndex = 0;

  function loop(curTotal, curIndex) {

    let pageCount = Math.min(curTotal , once);

    requestAnimationFrame(function () {
      let fragement = document.createDocumentFragment();
      for (let i = 0; i < pageCount; i++) {
        let li = document.createElement("li");
        li.innerText =
          "index: " + res[curIndex + i].index + "; text: " + res[curIndex + i].text;
        fragement.appendChild(li);
      }

      ul.appendChild(fragement);
      loop(curTotal - pageCount, curIndex + pageCount);
    });
  }

  loop(total, curIndex);
}

方案三:setTimeout + Generator

function request() {
  let result = [];
  for (let i = 0; i < 100000; i++) {
    result.push({
      index: i,
      text: ~~(Math.random() * 100000000),
    });
  }
  return Promise.resolve(result);
}

let ul = document.querySelector("#app");

request().then((res) => {
  function ts(gen) {
    if (typeof gen === "function") gen = gen();
    if (!gen || typeof gen.next !== "function") return;
    return function next() {
      const res = gen.next();
      if (res.done) return;
      requestAnimationFrame(next);
    };
  }

  let total = res.length;
  let once = 20;
  let page = total / once;
  let curIndex = 0;

  ts(addDom(total))();

  function* addDom(curTotal) {
    while (curTotal > 0) {
      let pageCount = Math.min(curTotal , once);

      let fragement = document.createDocumentFragment();
      for (let i = 0; i < pageCount; i++) {
        let li = document.createElement("li");
        li.innerText =
          "index: " +
          res[curIndex + i].index +
          "; text: " +
          res[curIndex + i].text;
        fragement.appendChild(li);
      }
      ul.appendChild(fragement);
      curIndex = curIndex + pageCount;
      curTotal = curTotal - pageCount;
      yield;
    }
  }

参考:
「前端进阶」高性能渲染十万条数据(时间分片)
时间切片(Time Slicing)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值