使用时间分片来解决10万个同步任务要执行

281 阅读4分钟

问题:经常面试被问到,如果有10万个任务要执行,或者一个任务要被执行10万次,该怎么办?

都知道js是单线程,如果js一直占用主线程,那么浏览器就不能响应用户的交互,从而产生卡顿。因此我们需要让浏览器在执行任务时,需要暂停,从而去响应用户的操作。具体的实现方案有这3种 第一种,使用requestAnimationFrame,具体如何做,就不细说。 第二种,使用new worker新起一个线程去执行。 第三种,使用时间分片来执行。这也是我今天想要讲的。

问题:假设现在有一个计数器count,我想让它从0自增到100万,并且要增加的足够快。用户点击页面按钮时,显示当前cout的值。

针对这个问题,首先我们想的的是whlie循环,但是whlie会阻塞用交互,所以直接放弃。其次我们可以使用setInterval来实现。但是需要知道的一点setInterval最快也需要间隔4ms才执行一次。因此也不是最快的方法。最后我们想到结合while和setInterval一起来实现。但是还有更快的方法,那就是while和MessageChannel来实现也就是我下面讲述的方案。react也有时间分片机制,它在浏览器支持MessageChannel时,就采用这种方案。不支持时,就采用setInterval这种方案实现的。

下面是几个知识点

MessageChannel

MessageChannel是js的一个类,调用这个它可以返回一个对象。这个对象包含两个prot,可以使用这两个prot来进行通信。当我们调用一次port1.postMessage()时,将相当于用户进行了一次点击,port2就会执行onmessage事件。并且和点击事件一样,onmessage事件也是一个宏任务。

let {port1, port2} = new MessageChannel();
// port1发起消息通知
port1.postMessage('123');
// port2接受通知,并触发onmessage事件,并且这个事件是一个宏任务
port2.onmessage = (data) => {
    console.log(data)
};

时间分片原理及步骤:

  • 1.创建一个宏任务
  • 2.记录一下任务开始的时间戳
  • 3.然后设置一个宏任务允许执行的最大时长
  • 4.计算出宏任务执行的终止时间
  • 5.在宏任务中去执行这些同步任务,并且每执行完一个同步任务后,都需要判断当前时间是否超过了截止时间。
  • 6.如果没有超过截止时间,则接着执行下一个任务
  • 7.如果超过截止时间,则重新创建一个宏任务,并重复上述过程(也就是需要递归调用)。

具体实现

// 用来记录任务的开始时间
let startTime;
// 定义一个常量,用来设置一个宏任务执行的时长
const during = 5;
// 用来记录任务的截止时间
let endTime;
// 创建两个端口,用来创建宏任务
let {port1, port2} = new MessageChannel();
// 计数器
let count = 0;
// 需要执行的自增任务
function work() {
    count++
}
// 判断当前时间是否超过了截止时间,也就是上述5,6过程
function shouldStop() {
    return window.performance.now() > endTime;
}
// 用来创建一个宏任务
function startWork() {
    // 记录当前宏任务的开始时间
    startTime = window.performance.now();
    // 计算出当前宏任务的截止时间
    endTime = startTime + during;
    // 创建一个宏任务,可以理解成用户进行了一次点击操作
    port1.postMessage(null)
}
// 宏任务,在内部执行我们的自增任务,可以理解成响应用户的点击操作
function performWork() {
    // 判断是否需要终止当前宏任务,或者任务已经执行完毕
    while (!shouldStop() && count < 100000000) {
        work()
    }
    // 当前宏任务已经到了截止时间,如果任务还没有执行完毕,则需要重新创建一个宏任务,需要递归调用
    if(count < 100000000){
        startWork();
    }
}

// 给port2绑定事件,用来响应port1的postMessage操作
port2.onmessage = performWork;

// 获取dom节点
let a = document.getElementById('a');
let b = document.getElementById('b');

// 用户的点击操作
a.addEventListener('click', () => {
    let p = document.createElement('p')
    p.innerText = String(count)
    b.appendChild(p)
})
// 开始任务
startWork();

贴上对应的html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button id="a">add item</button>
<div id="b"></div>
</body>
</html>

最后我们通过火焰图来看一下实际的效果

image.png

可以看到我们点击时,页面不会卡顿,计数器也在按照我们期待的样子,快速自增。并且我们可以看到有很多task,这些都是一个又一个的宏任务。在这些task之间,浏览器是可以去执行他的渲染操作,和响应用户的点击事件。 以上便讲解完毕,如有什么错误的地方,请留言