我们知道,js 是单线程的脚本语言,单线程的特点就是所有代码只能按照线性先后顺序,同步的被执行。
即使我们说 js 可以执行异步任务,但本质是这些异步任务只是放在了等候队列,依然需要等待同步队列执行完成后,才能被加入到同步队列中,同步的去执行。
因此即使将计算任务放到异步队列中,它最终依然会被同步的执行,那么整个计算过程就会阻塞主线程的执行。
比如这样一个场景:点击"开始计算"按钮 执行1000*1000次的打印任务,紧接着点击"计时器"按钮
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>同步执行计算任务</title>
<style>
html, body {
text-align: center;
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
</style>
</head>
<body>
<button id="clickMe">开始计算</button>
<br />
<button id="counter" style="margin-top: 20px;">Counter: 0</button>
<script>
const clickBtn = document.getElementById('clickMe');
const counterBtn = document.getElementById('counter');
let counter = 0;
clickBtn.addEventListener('click', () => {
calc();
});
counterBtn.addEventListener('click', () => {
counter ++;
console.log(`%cCounter Update: ${counter}`, 'color: green;');
counterBtn.innerText = `Counter: ${counter}`
});
function calc() {
for (let i = 0; i < 1000; i++) {
console.log(`%cOuter Calc: ${i}`, 'color: gray;');
for (let j = 0; j < 1000; j++) {
console.debug(`${i}-${j}`);
}
}
}
</script>
</body>
</html>
在执行计算任务的过程中,会发现点击"计时器"按钮的事件长时间得不到响应,整个js线程都被阻塞在执行中的计算任务,也就是我们所说的卡死状态。直到任务计算完成,卡死状态才会结束,事件才会得到响应。
如果真实业务中存在这种大量的计算,且实现方式跟上面一样,可想而知,交互体验将会变的很糟糕!
假如我们的计算任务不能通过算法进行提效,那么应该怎么解决因大量计算导致的交互卡顿或卡死的问题呢?
下面有几种方案可供参考:
-
使用
webworker
,开启子线程执行计算任务 -
使用
requestIdleCallback
在主线程中,进行空闲时分片计算 -
如果业务可行,将计算任务交给其他端(如:BS架构的服务端)
webworker
虽然可以多线程并发,但是跨线程数据传输的性能损耗和瓶颈,对于数据量大的时候,带来的问题也会很明显。
我们这里主要分析,计算任务必须放在前端处理的场景,所以会采用方案:requestIdleCallback
空闲时分片计算进行举例。
依然是上面的例子,我们使用requestIdleCallback
空闲时接口进行改造
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>空闲时-分片执行计算任务</title>
<style>
html, body {
text-align: center;
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
</style>
</head>
<body>
<button id="clickMe">开始计算</button>
<br />
<button id="counter" style="margin-top: 20px;">Counter: 0</button>
<script>
const clickBtn = document.getElementById('clickMe');
const counterBtn = document.getElementById('counter');
let counter = 0;
clickBtn.addEventListener('click', () => {
calc();
});
counterBtn.addEventListener('click', () => {
counter ++;
console.log(`%cCounter Update:${counter}`, 'color: green;');
counterBtn.innerText = `Counter: ${counter}`
});
function calc() {
let index = 0;
// 将大的计算任务,拆分为小的计算任务,通过空闲时去分片执行
function innerCalc(i) {
console.log(`%cOuter Calc: ${i}`, 'color: gray;');
for (let j = 0; j < 1000; j++) {
console.debug(`${i}-${j}`);
}
}
function outerCalc(deadline) {
// 如果有空闲时间,或者计算等待超时,则执行计算任务
while (index < 1000 && (deadline.timeRemaining() > 0 || deadline.timeout)) {
innerCalc(index);
index ++;
}
if (index < 1000) {
requestIdleCallback(outerCalc, {timeout: 1000});
} else {
console.log("任务执行结束");
}
}
requestIdleCallback(outerCalc, { timeout: 1000 });
}
</script>
</body>
</html>
可以看出来,当我们使用空闲时计算时,虽然计算任务一直在持续,但是并没有阻塞 "计时器" 的交互和更新。这会给交互体验,带来很大的改善。
使用空闲时执行计算任务,实现交互体验优化的关键有两个点:
- 将计算任务的优先级降低,加入异步等待队列
- 将大的计算任务拆分为小的任务,分片执行,当任务被执行时,不会长时间用户交互行为