sob
Version:
Schedule on Browser through single rAF and rIC mechanism
325 lines (305 loc) • 9.09 kB
JavaScript
/*!
Copyright (C) 2016 by Andrea Giammarchi @WebReflection
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
var
// local shortcuts
type,
hidden,
tabIsVisible = true,
onceVisible = [],
performance = (
global.performance ||
{now: Date.now}
),
now = (
performance.now ||
performance.webkitNow ||
function now() { return (new Date()).getTime(); }
),
max = Math.max,
requestAnimationFrame = (
global.requestAnimationFrame ||
global.webkitRequestAnimationFrame ||
global.mozRequestAnimationFrame ||
function (fn) { timeout(fn, 16); }
),
requestIdleCallback = (
global.requestIdleCallback ||
function (fn, options) {
var
// schedule timeout at least in the next frame
fps = 1000 / next.minFPS,
// grab shceduling time
st = time(),
t
;
timeout(function () {
// grab time before the next "tick"
t = time();
timeout(function () {
fn({
// when this happens, forces at least one task to be executed no matter what
didTimeout: options.timeout < (time() - st),
// returns how much time left
timeRemaining: function () {
return max(0, next.minFPS - (time() - t));
}
}, 1);
});
}, fps);
}
),
clear = global.clearInterval,
timeout = global.setTimeout,
// exported module
next = {
// if true, shows "frame overload" when it happens
debug: false,
// when operations slow down FPS is true
isOverloaded: false,
// minimum accepted FPS (suggested range 20 to 60)
minFPS: 60,
// maximum delay per each requestIdleCallback operation
maxIdle: 2000,
// remove a scheduled frame, idle, or timer operation
clear: function (id) {
return typeof id === 'number' ?
clear(id) :
void(
drop(qframe, id) ||
drop(qidle, id) ||
drop(qframex, id) ||
drop(qidlex, id)
);
},
// schedule a callback for the next frame
// returns its unique id as object
// .frame(callback[, arg0, arg1, argN]):object
frame: function frame() {
if (!frameRunning) {
frameRunning = true;
requestAnimationFrame(animationLoop);
}
return create.apply(qframe, arguments);
},
// schedule a callback for the next idle callback
// returns its unique id as object
// .idle(callback[, arg0, arg1, argN]):object
idle: function idle() {
if (!idleRunning) {
idleRunning = true;
requestIdleCallback(idleLoop, {timeout: next.maxIdle});
}
return create.apply(qidle, arguments);
},
interval: createTimer(global.setInterval),
timeout: createTimer(timeout),
now: now
},
// local variables
// rAF and rIC states
frameRunning = false,
idleRunning = false,
// animation frame and idle queues
qframe = [],
qidle = [],
// animation frame and idle execution queues
qframex = [],
qidlex = []
;
// asliases
next.raf = next.frame;
next.ric = next.idle;
switch (true) {
case 'hidden' in document:
hidden = 'hidden';
type = 'visibilitychange';
break;
case 'msHidden' in document:
hidden = 'msHidden';
type = 'msvisibilitychange';
break;
case 'webkitHidden' in document:
hidden = 'webkitHidden';
type = 'webkitvisibilitychange';
break;
}
if (hidden) {
document.addEventListener(type, onVisibility, false);
onVisibility();
}
// responsible for centralized requestAnimationFrame operations
function animationLoop() {
var
// grab current time
t = time(),
// calculate how many millisends we have
fps = 1000 / next.minFPS,
// used to flag overtime in case we exceed milliseconds
overTime = false,
// take current frame queue length
length = getLength(qframe, qframex)
;
// if there is actually something to do
if (length) {
// reschedule upfront next animation frame
// this prevents the need for a try/catch within the while loop
requestAnimationFrame(animationLoop);
// reassign qframex cleaning current animation frame queue
qframex = qframe.splice(0, length);
while (qframex.length) {
// if some of them fails, it's OK
// next round will re-prioritize the animation frame queue
exec(qframex.shift());
// if we exceeded the frame time, get out this loop
overTime = (time() - t) >= fps;
if ((next.isOverloaded = overTime)) break;
}
// if overtime and debug is true, warn about it
if (overTime && next.debug) console.warn('overloaded frame');
} else {
// all frame callbacks have been executed
// we can actually stop asking for animation frames
frameRunning = false;
// and flag it as non busy/overloaded anymore
next.isOverloaded = frameRunning;
}
}
// create a unique id and returns it
// if the callback with same extra arguments
// was already scheduled, then returns same id
function create(callback) {
/* jslint validthis: true */
for (var
queue = this,
args = [],
info = {
id: {},
fn: callback,
ar: args
},
i = 1; i < arguments.length; i++
) args[i - 1] = arguments[i];
return infoId(queue, info) || (queue.push(info), info.id);
}
// create setTimeout or setInterval wrapper
function createTimer(timer) {
return function (fn) {
// overwrite the function with one that
// execute only if the tab is visible
// scheduling eventually for later and once
// in case the tab is not
arguments[0] = function () {
if (tabIsVisible) fn.apply(null, arguments);
else if (onceVisible.indexOf(fn) < 0) {
onceVisible.push(fn, arguments);
}
};
return timer.apply(null, arguments);
};
}
// remove a scheduled id from a queue
function drop(queue, id) {
var
i = findIndex(queue, id),
found = -1 < i
;
if (found) queue.splice(i, 1);
return found;
}
// execute a shceduled callback with optional args
function exec(info) {
info.fn.apply(null, info.ar);
}
// find queue index by id
function findIndex(queue, id) {
var i = queue.length;
while (i-- && queue[i].id !== id);
return i;
}
// return the right queue length to consider
// re-prioritizing scheduled callbacks
function getLength(queue, queuex) {
// if previous call didn't execute all callbacks
return queuex.length ?
// reprioritize the queue putting those in front
queue.unshift.apply(queue, queuex) :
queue.length;
}
// responsible for centralized requestIdleCallback operations
function idleLoop(deadline) {
var
length = getLength(qidle, qidlex),
didTimeout = deadline.didTimeout
;
if (length) {
// reschedule upfront next idle callback
requestIdleCallback(idleLoop, {timeout: next.maxIdle});
// this prevents the need for a try/catch within the while loop
// reassign qidlex cleaning current idle queue
qidlex = qidle.splice(0, didTimeout ? 1 : length);
while (qidlex.length && (didTimeout || deadline.timeRemaining()))
exec(qidlex.shift());
} else {
// all idle callbacks have been executed
// we can actually stop asking for idle operations
idleRunning = false;
}
}
// return a scheduled unique id through similar info
function infoId(queue, info) {
for (var i = 0, length = queue.length, tmp; i < length; i++) {
tmp = queue[i];
if (
tmp.fn === info.fn &&
sameValues(tmp.ar, info.ar)
) return tmp.id;
}
return null;
}
function onVisibility() {
tabIsVisible = !document[hidden];
for (var
length = onceVisible.length,
list = onceVisible.splice(0, length),
i = 0; i < length; i += 2
) {
list[i].apply(null, list[i + 1]);
}
}
// compare two arrays values
function sameValues(a, b) {
var
i = a.length,
j = b.length,
k = i === j
;
if (k) {
while (i--) {
if (a[i] !== b[i]) {
return !k;
}
}
}
return k;
}
// return performance.now() real or sham value
function time() {
return now.call(performance);
}
module.exports = next;