mini-scheduler
Version:
A mini scheduler for task handler in browser
166 lines (162 loc) • 4.99 kB
JavaScript
/*!
* mini-scheduler v1.0.0
* (c) 2021 bryce
* @license MIT
*/
const getAsyncFunc = (type) => {
let func;
switch (type) {
case 'setTimeout':
func = window.setTimeout;
break;
case 'requestAnimationFrame':
func = window.requestAnimationFrame;
break;
default:
throw new Error("currently only support 'setTimeout' | 'requestAnimationFrame' types");
}
return func;
};
const getTime = () => performance.now();
const clearTimers = (timerQueue, clearTimerFunc) => {
let _timer = timerQueue.pop();
while (timerQueue.length && _timer) {
clearTimerFunc(_timer);
_timer = timerQueue.pop();
}
};
const p = Promise.resolve();
const nextTick = (fn) => p.then(fn);
const isGenerator = (target) => {
if (!target)
return false;
return target.toString().includes('Generator');
};
const queue = [];
let queued = false;
let fps = 60;
let renderTime = 10;
let triggerFunc = window.setTimeout;
const timerQueue = [];
const shouldYield = (startTime) => {
return (navigator?.scheduling?.isInputPending() ||
getTime() < startTime + 1000 / fps - renderTime);
};
const stepFunc = (generator) => {
const step = () => {
const start = getTime();
let temp = null;
do {
temp = generator.next();
} while (!temp.done && shouldYield(start));
if (temp.done) {
const clearTimerFunc = triggerFunc === window.setTimeout
? window.clearTimeout
: window.cancelAnimationFrame;
clearTimers(timerQueue, clearTimerFunc);
return;
}
const timerId = triggerFunc(step);
timerQueue.push(timerId);
};
return step;
};
function* flushJobs() {
for (let i = 0; i < queue.length; i++) {
const func = queue[i];
const res = func();
if (isGenerator(res)) {
const timerId = triggerFunc(stepFunc(res));
timerQueue.push(timerId);
}
else {
yield res;
}
}
queue.length = 0;
queued = false;
}
const queueFlushJobs = () => {
const generator = flushJobs();
return function next() {
const start = getTime();
let res = null;
do {
res = generator.next();
} while (!res.done && shouldYield(start));
if (res.done) {
const clearTimerFunc = triggerFunc === window.setTimeout
? window.clearTimeout
: window.cancelAnimationFrame;
clearTimers(timerQueue, clearTimerFunc);
return;
}
const timerId = triggerFunc(next);
timerQueue.push(timerId);
};
};
const queueJob = (job) => {
if (!queue.includes(job))
queue.push(job);
if (!queued) {
queued = true;
nextTick(queueFlushJobs());
}
};
const judgeConfig = (config) => {
const { fps: _fps = 60, renderTime: _renderTime = 10, funcType: _funcType = 'setTimeout', } = config;
if (fps <= 0 || fps > 120) {
throw new Error(`fps must between 0~120, instead of ${fps}`);
}
if (renderTime <= 0 || renderTime >= 1000 / 60) {
throw new Error(`fps must between 0~${Math.floor(1000 / 60)}, instead of ${renderTime} ms`);
}
if (!['setTimeout', 'requestAnimationFrame'].includes(_funcType)) {
throw new TypeError('funcType must be window.setTimeout or window.requestAnimationFrame');
}
fps = _fps;
renderTime = _renderTime;
triggerFunc =
_funcType === 'setTimeout'
? window.setTimeout
: window.requestAnimationFrame;
};
const ts = (arr, func, config = {
fps: 60,
renderTime: 10,
funcType: 'setTimeout',
}) => {
judgeConfig(config);
for (let i = 0; i < arr.length; i++) {
queueJob(() => func(arr[i], i, arr));
}
};
const tsGenerator = (func, config = {
fps: 60,
renderTime: 10,
funcType: 'setTimeout',
}) => {
if (!isGenerator(func)) {
throw new TypeError('payload func must be a generator ');
}
judgeConfig(config);
const timerId = triggerFunc(stepFunc(func));
timerQueue.push(timerId);
};
const asyncPool = async (limit, arr, func) => {
const ret = [];
const executing = [];
for (const item of arr) {
const p = Promise.resolve().then(() => func(item, arr));
ret.push(p);
if (limit <= arr.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return await Promise.all(ret);
};
export { asyncPool, clearTimers, getAsyncFunc, getTime, isGenerator, nextTick, ts, tsGenerator };