前言
generator函数对于大多数前端来说可能熟悉又陌生。作为实现async/await的底层方法,在实际开发过程中,可能完全被async/await方法替代了。本文便记录一种generator函数在实际优化场景中的使用案例。
代码分析
测试场景搭建
// html(部分代码)
<div class="container">
<div>
<span>总循环次数</span>
<input type="number" id="loopCount" value="10000000" step="100000" />
</div>
<div class="topBox">
<div class="part">
<button onclick="directExecute('d1', 'simple')">一帧搞定</button>
</div>
</div>
<div class="resultBox">
<div id="d1"></div>
</div>
<div class="aniBox">
<div class="ani-obj"></div>
</div>
</div>
// css
@keyframes slidein {
from {
margin-left: 0;
}
to {
margin-left: 90%;
}
}
.aniBox {
width: 100%;
background-color: rgb(169, 169, 127);
position: relative;
}
.ani-obj {
width: 60px;
height: 60px;
position: relative;
background-color: coral;
animation-duration: 1s;
animation-name: slidein;
animation-iteration-count: infinite;
animation-direction: alternate;
}
// js
const getLoopCount = function () {
const el = document.querySelector("#loopCount");
let loopCount = 10000000;
if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
loopCount = Number(el.value);
}
return loopCount;
}
const resetDom = function (id) {
const element = document.getElementById(id);
element.innerText = "";
element.style.backgroundColor = "rgb(224, 224, 224)";
}
// 简单的计算
const simple = function (i) {
return i * 2 + Math.abs(i - 600) - Math.cos(i) + Math.sin(i);
}
// 复杂一些的计算
const complex = function (i) {
return i * 2 + Math.abs(i - 600) - (Math.cos(i) + Math.sin(i)) * (1 + Math.sqrt(i)) + Math.sin(i + 1) + Math.cos(i + 3);
}
// 计算函数获取对象
const taskObj = {
simple,
complex
}
// 定义一个循环函数,计算结果并展示,计算过程在一帧之内完成
const directExecute = function (textID, type) {
console.time('directExecute');
resetDom(textID); // 此项应当不成功,因为进程卡住了,test2和test3的可以
const loopCount = getLoopCount();
const directExecute = function () {
let result = 0;
for (let i = 0; i < loopCount; i++) {
result += taskObj[type](i);
}
const element = document.getElementById(textID);
element.innerText = result;
element.style.backgroundColor = "cornflowerblue";
console.timeEnd('directExecute');
};
directExecute();
};
展示效果如下:
其实就是同步的大量计算,可以在gif中看到,每一次点击都会使得页面卡顿,这也是常见的前端性能问题。
初步优化
增加下列逻辑
// html
<div class="part">
<input type="number" id="t2" value="500000" step="100000" />
<button onclick="firstGenerator('t2', 'd2', 'simple')">分帧</button>
</div>
// js
const firstGenerator = function (inputID, textID, type) {
console.time('firstGenerator');
resetDom(textID);
const loopCount = getLoopCount();
const frameExecute = function* (maxCount) {
// 执行计数
let count = 0;
let result = 0;
for (let i = 0; i < loopCount; i++) {
result += taskObj[type](i);
// 以下为分帧相关逻辑
count++; // 每次循环计数+1
if (count >= maxCount) {
count = 0; // 达到最大次数后,yield
yield;
}
}
const element = document.getElementById(textID);
element.innerText = result;
element.style.backgroundColor = "cornflowerblue";
console.timeEnd('firstGenerator');
};
// 每frame执行多少次
const el = document.querySelector(`#${inputID}`);
let maxCountPerFrame = 3;
if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
maxCountPerFrame = Number(el.value);
}
const gen = frameExecute(maxCountPerFrame);
const e = function () {
const iter = gen.next();
if (iter.done) {
console.log('结束!!!');
return;
}
requestAnimationFrame(e);
};
requestAnimationFrame(e);
};
运行结果如下:
分析: 使用generator结合requestAnimationFrame方法,将大量的运算逻辑分在各个帧的空闲时段处理。如图所示,在使用分帧逻辑进行运算时,下面的动画完全没有卡顿。
问题:
- 有人可能会说,这种大量计算的逻辑,用worker也可以实现,可能还更为标准便利。
- 这只是纯数据计算的场景,那么在那些浏览器的功能场景下能否使用这种方式呢?
在浏览器功能场景下的案例
下面在代码中添加功能交互: element.innerText = result,动态展示计算数据
const secondGenerator = function (inputID, textID, type) {
resetDom(textID);
const loopCount = getLoopCount();
const frameExecute = function* (maxCount) {
// 执行计数
let count = 0;
let result = 0;
const element = document.getElementById(textID);
for (let i = 0; i < loopCount; i++) {
count++; // 每次循环计数+1
result += taskObj[type](i);
element.innerText = result; // 非常卡!!!
if (count >= maxCount) {
count = 0; // 达到最大次数后,yield
yield;
}
}
element.style.backgroundColor = "cornflowerblue";
console.timeEnd('secondGenerator');
};
// 每frame执行多少次
const el = document.querySelector(`#${inputID}`);
let maxCountPerFrame = 3;
if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
maxCountPerFrame = Number(el.value);
}
const gen = frameExecute(maxCountPerFrame);
const e = function () {
const iter = gen.next();
if (iter.done) {
console.log('结束!!!');
return;
}
requestAnimationFrame(e);
};
requestAnimationFrame(e);
};
执行结果如下:
可以看出,加入dom操作后,整个流程又变的非常卡顿。毕竟重绘的消耗和纯运算不是一个数量级。
但是如果把每帧计算次数调低,还是比较流畅的:
但是强制将执行次数缩小到一定范围,又会大大增加完成整个逻辑流程的时间,有没有什么更合适的方式呢?
动态调整执行次数
增加动态调整执行次数的逻辑,根据当前帧率,如果保持在正常范围(16-17)或者更大则适当增加执行次数,如果小于正常情况(暂定16)则减小当前次数。
const thirdGenerator = function (inputID, textID, type) {
resetDom(textID);
const loopCount = getLoopCount();
const frameExecute = function* (maxCount) {
// 执行计数
let count = 0;
let result = 0;
let currentMaxCount = maxCount
const element = document.getElementById(textID);
for (let i = 0; i < loopCount; i++) {
result += taskObj[type](i);
// 以下为分帧相关逻辑
count++; // 每次循环计数+1
element.innerText = result;
if (count >= currentMaxCount) {
count = 0; // 达到最大次数后,yield
currentMaxCount = Math.round(currentMaxCount * (yield));
}
}
element.style.backgroundColor = "cornflowerblue";
console.timeEnd('thirdGenerator');
};
const createAdjustMaxCount = function() {
const normalFPS = 60;
const standard = 1000 / normalFPS;
let frequentTimeList = [];
const frequentNum = 5;
let lastTime = performance.now();
return function() {
const currentTime = performance.now();
frequentTimeList.push(currentTime - lastTime);
lastTime = currentTime;
if(frequentTimeList.length >= frequentNum) {
const currentFrequentTime = frequentTimeList.reduce((sum, next) => sum + next) / frequentTimeList.length;
frequentTimeList = [];
if(currentFrequentTime > standard + 0.5) {
return 0.9
} else {
return 1.1
}
}
return 1;
}
}
// 每frame执行多少次
const el = document.querySelector(`#${inputID}`);
let maxCountPerFrame = 3;
if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
maxCountPerFrame = Number(el.value);
}
const gen = frameExecute(maxCountPerFrame);
const adjustMaxCount = createAdjustMaxCount();
const e = function () {
const iter = gen.next(adjustMaxCount());
if (iter.done) {
console.log('结束!!!');
return;
}
requestAnimationFrame(e);
};
requestAnimationFrame(e);
};
结果如下:
可以看到,在初始以500次/帧的起始情况下,跨度到达8000-10000的处理频率,既不会导致卡顿,时间也有所保证,还能兼容用户在操作情况下的性能变化。至此整个应用场景完成。