├── template ├── node.before ├── license.before ├── license.after ├── md.after ├── amd.after ├── node.after ├── var.after ├── copyright ├── var.before ├── amd.before └── md.before ├── .gitignore ├── .travis.yml ├── .npmignore ├── utils ├── browserify.sh ├── watchify.sh ├── uglifyjs.sh └── jshint.sh ├── package.json ├── LICENSE.txt ├── test ├── .test.js └── sob.js ├── testrunner.js ├── README.md ├── Makefile ├── index.html └── src └── sob.js /template/node.before: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/license.before: -------------------------------------------------------------------------------- 1 | /*! 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /template/license.after: -------------------------------------------------------------------------------- 1 | 2 | */ 3 | -------------------------------------------------------------------------------- /template/md.after: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /template/amd.after: -------------------------------------------------------------------------------- 1 | 2 | return next; 3 | 4 | }); -------------------------------------------------------------------------------- /template/node.after: -------------------------------------------------------------------------------- 1 | 2 | module.exports = next; -------------------------------------------------------------------------------- /template/var.after: -------------------------------------------------------------------------------- 1 | 2 | return next; 3 | 4 | }(window)); -------------------------------------------------------------------------------- /template/copyright: -------------------------------------------------------------------------------- 1 | /*! (C) WebReflection Mit Style License */ 2 | -------------------------------------------------------------------------------- /template/var.before: -------------------------------------------------------------------------------- 1 | var sob = (function (global) {'use strict'; 2 | -------------------------------------------------------------------------------- /template/amd.before: -------------------------------------------------------------------------------- 1 | define(function () { 2 | 3 | var global = window; 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 0.12 5 | - 4 6 | - 6 7 | git: 8 | depth: 1 9 | branches: 10 | only: 11 | - master -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .travis.yml 3 | src/* 4 | test/* 5 | template/* 6 | template/license.after 7 | template/license.before 8 | utils/* 9 | node_modules/* 10 | build/*.amd.js 11 | Makefile 12 | index.html 13 | testrunner.js -------------------------------------------------------------------------------- /utils/browserify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | program () { 4 | local bin="$1" 5 | if [ -d "node_modules/$bin" ]; then 6 | bin="$2" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing $1" 11 | npm install $bin >/dev/null 2>&1 12 | bin="$2" 13 | fi 14 | fi 15 | echo $bin 16 | } 17 | 18 | run () { 19 | local bin="$(program 'browserify' 'node_modules/browserify/bin/cmd.js')" 20 | $bin src/main.js -o build/bundle.max.js -d 21 | } 22 | 23 | run -------------------------------------------------------------------------------- /utils/watchify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | program () { 4 | local bin="$1" 5 | if [ -d "node_modules/$bin" ]; then 6 | bin="$2" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing $1" 11 | npm install $bin >/dev/null 2>&1 12 | bin="$2" 13 | fi 14 | fi 15 | echo $bin 16 | } 17 | 18 | run () { 19 | local bin="$(program 'watchify' 'node_modules/watchify/bin/cmd.js')" 20 | $bin src/main.js -o build/bundle.max.js -v 21 | } 22 | 23 | run 24 | -------------------------------------------------------------------------------- /utils/uglifyjs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | run () { 4 | local bin="uglifyjs" 5 | if [ -d "node_modules/uglify-js" ]; then 6 | bin="node_modules/uglify-js/bin/uglifyjs" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing uglify-js" 11 | npm install "uglify-js@1" >/dev/null 2>&1 12 | bin="node_modules/uglify-js/bin/uglifyjs" 13 | fi 14 | fi 15 | echo "$@" >build/bundle.js 16 | $bin --verbose build/bundle.max.js >>build/bundle.js 17 | 18 | } 19 | 20 | run "$@" -------------------------------------------------------------------------------- /template/md.before: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "license": "MIT", 4 | "name": "sob", 5 | "description": "Schedule on Browser through single rAF and rIC mechanism", 6 | "homepage": "https://github.com/WebReflection/sob", 7 | "keywords": [ 8 | "rAF", 9 | "rIC", 10 | "requestAnimationFrame", 11 | "requestIdleCallback", 12 | "setTimeout", 13 | "performance" 14 | ], 15 | "author": { 16 | "name": "Andrea Giammarchi", 17 | "web": "http://webreflection.blogspot.com/" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/WebReflection/sob.git" 22 | }, 23 | "main": "./build/sob.node.js", 24 | "scripts": { 25 | "test": "phantomjs testrunner.js", 26 | "web": "tiny-cdn run -p=1337" 27 | }, 28 | "devDependencies": { 29 | "jshint": "^2.11.0", 30 | "phantomjs-prebuilt": "^2.1.16", 31 | "tiny-cdn": "^0.7.0", 32 | "uglify-js": "^3.7.7", 33 | "wru": "^0.3.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /utils/jshint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | program () { 4 | local bin="$1" 5 | if [ -d "node_modules/$bin" ]; then 6 | bin="$2" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing $1" 11 | npm install $bin >/dev/null 2>&1 12 | bin="$2" 13 | fi 14 | fi 15 | echo $bin 16 | } 17 | 18 | run () { 19 | local bin="$(program 'jshint' 'node_modules/jshint/bin/jshint')" 20 | local folder=$1 21 | local js="" 22 | # drop some info in the stdout 23 | echo "linting $f ... " 24 | for f in $folder/*; do 25 | # if it's a fodler, go for recursion 26 | if [ -d "$f" ]; then 27 | run "$f" 28 | else 29 | # grab .js files only 30 | js=$(echo "$f" | sed 's/.js//') 31 | if [ "$js" != "$f" ]; then 32 | # finally use jshint to verify the file 33 | $bin "$f" 34 | # in case there was an error 35 | if [[ $? -ne 0 ]] ; then 36 | exit 1 37 | fi 38 | fi 39 | fi 40 | done 41 | } 42 | 43 | run src -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 by Andrea Giammarchi @WebReflection 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/.test.js: -------------------------------------------------------------------------------- 1 | var 2 | fs = require('fs'), 3 | path = require('path'), 4 | spawn = require('child_process').spawn, 5 | modules = path.join(__dirname, '..', 'node_modules', 'wru', 'node', 'program.js'), 6 | tests = [], 7 | ext = /\.js$/, 8 | code = 0, 9 | many = 0; 10 | 11 | function exit($code) { 12 | if ($code) { 13 | code = $code; 14 | } 15 | if (!--many) { 16 | if (!code) { 17 | fs.writeFileSync( 18 | path.join(__dirname, '..', 'index.html'), 19 | fs.readFileSync( 20 | path.join(__dirname, '..', 'index.html'), 21 | 'utf-8' 22 | ).replace(/var TESTS = \[.*?\];/, 'var TESTS = ' + JSON.stringify(tests) + ';'), 23 | 'utf-8' 24 | ); 25 | } 26 | process.exit(code); 27 | } 28 | } 29 | 30 | fs.readdirSync(__dirname).filter(function(file){ 31 | if (ext.test(file) && (fs.existsSync || path.existsSync)(path.join(__dirname, '..', 'src', file))) { 32 | ++many; 33 | tests.push(file.replace(ext, '')); 34 | spawn( 35 | 'node', [modules, path.join('test', file)], { 36 | detached: false, 37 | stdio: [process.stdin, process.stdout, process.stderr] 38 | }).on('exit', exit); 39 | } 40 | }); -------------------------------------------------------------------------------- /testrunner.js: -------------------------------------------------------------------------------- 1 | console.log('Loading: test.html'); 2 | var page = require('webpage').create(); 3 | var url = 'index.html'; 4 | page.open(url, function (status) { 5 | if (status === 'success') { 6 | setTimeout(function () { 7 | var results = page.evaluate(function() { 8 | // remove the first node with the total from the following counts 9 | var passed = Math.max(0, document.querySelectorAll('.pass').length - 1); 10 | return { 11 | // retrieve the total executed tests number 12 | total: ''.concat( 13 | passed, 14 | ' blocks (', 15 | document.querySelector('#wru strong').textContent.replace(/\D/g, ''), 16 | ' single tests)' 17 | ), 18 | passed: passed, 19 | failed: Math.max(0, document.querySelectorAll('.fail').length - 1), 20 | failures: [].map.call(document.querySelectorAll('.fail'), function (node) { 21 | return node.textContent; 22 | }), 23 | errored: Math.max(0, document.querySelectorAll('.error').length - 1), 24 | errors: [].map.call(document.querySelectorAll('.error'), function (node) { 25 | return node.textContent; 26 | }) 27 | }; 28 | }); 29 | console.log('- - - - - - - - - -'); 30 | console.log('total: ' + results.total); 31 | console.log('- - - - - - - - - -'); 32 | console.log('passed: ' + results.passed); 33 | if (results.failed) { 34 | console.log('failures: \n' + results.failures.join('\n')); 35 | } else { 36 | console.log('failed: ' + results.failed); 37 | } 38 | if (results.errored) { 39 | console.log('errors: \n' + results.errors.join('\n')); 40 | } else { 41 | console.log('errored: ' + results.errored); 42 | } 43 | console.log('- - - - - - - - - -'); 44 | if (0 < results.failed + results.errored) { 45 | status = 'failed'; 46 | } 47 | phantom.exit(0); 48 | }, 5000); 49 | } else { 50 | phantom.exit(1); 51 | } 52 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scheduled on Browser [![build status](https://secure.travis-ci.org/WebReflection/sob.svg)](http://travis-ci.org/WebReflection/sob) 2 | ==================== 3 | 4 | This is a zero dependencies utility to manage, in a simple and fully cross browser way, calls to [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) and [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback). 5 | 6 | **New** in version `0.1.0` the ability to schedule regular timeouts and intervals only when the tab is visible, saving operations for when the user needs them, as opposite to regardless. 7 | 8 | Things this module does: 9 | 10 | * it [doesn't overload](https://medium.com/@paul_irish/requestanimationframe-scheduling-for-nerds-9c57f7438ef4#.dui1p8y4f) the `requestAnimationFrame` internal queue 11 | * it schedules `frame` or `idle` passing extra arguments if specified, same as `setTimeout` and `setInterval` or `setImmediate` 12 | * it avoids duplicated scheduling of the same callback with same optional arguments (TL;DR it does throttle) 13 | * it makes `cancelAnimationFrame` and `cancelIdleCallback` consistent across browsers via `clear` method and for both scheduled queue and currently executed one 14 | * it provides a similar `requestIdleCallback` mechanism for every browser 15 | * it schedules timeouts and intervals executed only [when the browser is visible](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API), ensuring no duplicated callbacks are executed once the visibility is back 16 | 17 | ### API 18 | Following all methods provided by the object returned via this module. 19 | ```js 20 | const sob = require('sob'); 21 | ``` 22 | 23 | #### `sob.frame(fn) => uniqueId` 24 | Schedules `fn` for the next possible `requestAnimationFrame` without compromising performance. 25 | It returns a unique id usable to `sob.clear(uid)` if necessary. 26 | 27 | #### `sob.frame(fn, arg1, arg2, argN) => uniqueId` 28 | Similar to `sob.frame(fn)`, it schedules `fn` for the next possible `requestAnimationFrame` invoking it with optionally provided arguments. 29 | 30 | #### `sob.idle(fn) => uniqueId` 31 | Schedules `fn` for the next possible `requestIdleCallback` without compromising performance. 32 | It returns a unique id usable to `sob.clear(uid)` if necessary. 33 | 34 | #### `sob.idle(fn, arg1, arg2, argN) => uniqueId` 35 | Similar to `sob.idle(fn)`, it schedules `fn` for the next possible `requestIdleCallback` invoking it with optionally provided arguments. 36 | 37 | #### `sob.interval(fn, delay, arg0, arg1, ....argN)` 38 | Schedule an interval that won't occur while the page is not visible, eventually delaying it for when it is without invoking it many times consecutively. 39 | 40 | #### `sob.timeout(fn, delay, arg0, arg1, ....argN)` 41 | Schedule a timeout that won't occur while the page is not visible, eventually delaying it for when it is. 42 | 43 | #### `sob.clear(uniqueId)` 44 | Remove a previously scheduled `frame`, `idle` or timer through the previously returned unique id. 45 | 46 | ### Examples 47 | Following a basic example on how to use this module. 48 | The TL;DR verion is that whenever you'd like to use `requestAnimationFrame` or `requestIdleCallback`, you can use `sob` instead. 49 | 50 | ```js 51 | // simulating a requestAnimationFrame task 52 | (function frameLoop() { 53 | sob.frame(frameLoop); 54 | document.body.textContent = new Date(); 55 | }()); 56 | 57 | // simulating a requestIdleCallback task 58 | (function idleLoop() { 59 | sob.idle(idleLoop); 60 | console.log('idle'); 61 | }()); 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build duk var node amd size hint clean test web preview pages dependencies 2 | 3 | # repository name 4 | REPO = sob 5 | 6 | # make var files 7 | VAR = src/$(REPO).js 8 | 9 | # make node files 10 | NODE = $(VAR) 11 | 12 | # make amd files 13 | AMD = $(VAR) 14 | 15 | # README constant 16 | 17 | 18 | # default build task 19 | build: 20 | make clean 21 | make var 22 | make node 23 | make amd 24 | make test 25 | make hint 26 | make size 27 | 28 | # build generic version 29 | var: 30 | mkdir -p build 31 | cat template/var.before $(VAR) template/var.after >build/no-copy.$(REPO).max.js 32 | node node_modules/.bin/uglifyjs --verbose build/no-copy.$(REPO).max.js >build/no-copy.$(REPO).js 33 | cat template/license.before LICENSE.txt template/license.after build/no-copy.$(REPO).max.js >build/$(REPO).max.js 34 | cat template/copyright build/no-copy.$(REPO).js >build/$(REPO).js 35 | rm build/no-copy.$(REPO).max.js 36 | rm build/no-copy.$(REPO).js 37 | 38 | # build node.js version 39 | node: 40 | mkdir -p build 41 | cat template/license.before LICENSE.txt template/license.after template/node.before $(NODE) template/node.after >build/$(REPO).node.js 42 | 43 | # build AMD version 44 | amd: 45 | mkdir -p build 46 | cat template/amd.before $(AMD) template/amd.after >build/no-copy.$(REPO).max.amd.js 47 | node node_modules/.bin/uglifyjs --verbose build/no-copy.$(REPO).max.amd.js >build/no-copy.$(REPO).amd.js 48 | cat template/license.before LICENSE.txt template/license.after build/no-copy.$(REPO).max.amd.js >build/$(REPO).max.amd.js 49 | cat template/copyright build/no-copy.$(REPO).amd.js >build/$(REPO).amd.js 50 | rm build/no-copy.$(REPO).max.amd.js 51 | rm build/no-copy.$(REPO).amd.js 52 | 53 | # build self executable for duktape 54 | duk: 55 | node -e 'var fs=require("fs");\ 56 | fs.writeFileSync(\ 57 | "test/duk.js",\ 58 | fs.readFileSync("node_modules/wru/build/wru.console.js") +\ 59 | "\n" +\ 60 | fs.readFileSync("build/$(REPO).js") +\ 61 | "\n" +\ 62 | fs.readFileSync("test/$(REPO).js").toString().replace(/^[^\x00]+?\/\/:remove\s*/,"")\ 63 | );' 64 | 65 | 66 | size: 67 | wc -c build/$(REPO).max.js 68 | gzip -c build/$(REPO).js | wc -c 69 | 70 | # hint built file 71 | hint: 72 | node node_modules/.bin/jshint build/$(REPO).max.js 73 | 74 | # clean/remove build folder 75 | clean: 76 | rm -rf build 77 | 78 | # tests, as usual and of course 79 | test: 80 | npm test 81 | 82 | # launch tiny-cdn (ctrl+click to open the page) 83 | web: 84 | node node_modules/.bin/tiny-cdn run 85 | 86 | # markdown the readme and view it 87 | preview: 88 | node_modules/markdown/bin/md2html.js README.md >README.md.htm 89 | cat template/md.before README.md.htm template/md.after >README.md.html 90 | open README.md.html 91 | sleep 3 92 | rm README.md.htm README.md.html 93 | 94 | pages: 95 | git pull --rebase 96 | git checkout gh-pages 97 | git pull --rebase 98 | git checkout master 99 | make var 100 | mkdir -p ~/tmp 101 | mkdir -p ~/tmp/$(REPO) 102 | cp .gitignore ~/tmp/ 103 | cp -rf src ~/tmp/$(REPO) 104 | cp -rf build ~/tmp/$(REPO) 105 | cp -rf test ~/tmp/$(REPO) 106 | cp index.html ~/tmp/$(REPO) 107 | git checkout gh-pages 108 | cp ~/tmp/.gitignore ./ 109 | mkdir -p test 110 | rm -rf test 111 | cp -rf ~/tmp/$(REPO) test 112 | git add .gitignore 113 | git add test 114 | git add test/. 115 | git commit -m 'automatic test generator' 116 | git push 117 | git checkout master 118 | rm -r ~/tmp/$(REPO) 119 | 120 | # modules used in this repo 121 | dependencies: 122 | rm -rf node_modules 123 | mkdir node_modules 124 | npm install wru 125 | npm install tiny-cdn 126 | npm install uglify-js@1 127 | npm install jshint 128 | npm install markdown 129 | npm install browserify 130 | npm install watchify 131 | npm install phantomjs-prebuilt 132 | 133 | # bundle: creates the browserified version of the project as js/bundle.max.js 134 | bundle: 135 | sh utils/browserify.sh 136 | 137 | # watch: update the browserified version of the project as soon as file changes 138 | watch: 139 | sh utils/watchify.sh 140 | 141 | # minified: create the minifeid version of the project as js/bundle.js 142 | minified: 143 | make -s bundle 144 | sh utils/uglifyjs.sh $(LICENSE) 145 | make -s size 146 | 147 | # jshint: recursively checks for javascript files inside the src folder and lint them 148 | jshint: 149 | sh utils/jshint.sh 150 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wru test 5 | 6 | 7 | 8 | 9 | 13 | 56 | 57 | 58 | 90 | 91 | 92 |
93 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/sob.js: -------------------------------------------------------------------------------- 1 | var 2 | // local shortcuts 3 | type, 4 | hidden, 5 | tabIsVisible = true, 6 | onceVisible = [], 7 | performance = ( 8 | global.performance || 9 | {now: Date.now} 10 | ), 11 | now = ( 12 | performance.now || 13 | performance.webkitNow || 14 | function now() { return (new Date()).getTime(); } 15 | ), 16 | max = Math.max, 17 | requestAnimationFrame = ( 18 | global.requestAnimationFrame || 19 | global.webkitRequestAnimationFrame || 20 | global.mozRequestAnimationFrame || 21 | function (fn) { timeout(fn, 16); } 22 | ), 23 | requestIdleCallback = ( 24 | global.requestIdleCallback || 25 | function (fn, options) { 26 | var 27 | // schedule timeout at least in the next frame 28 | fps = 1000 / next.minFPS, 29 | // grab shceduling time 30 | st = time(), 31 | t 32 | ; 33 | timeout(function () { 34 | // grab time before the next "tick" 35 | t = time(); 36 | timeout(function () { 37 | fn({ 38 | // when this happens, forces at least one task to be executed no matter what 39 | didTimeout: options.timeout < (time() - st), 40 | // returns how much time left 41 | timeRemaining: function () { 42 | return max(0, next.minFPS - (time() - t)); 43 | } 44 | }, 1); 45 | }); 46 | }, fps); 47 | } 48 | ), 49 | clear = global.clearInterval, 50 | timeout = global.setTimeout, 51 | // exported module 52 | next = { 53 | // if true, shows "frame overload" when it happens 54 | debug: false, 55 | // when operations slow down FPS is true 56 | isOverloaded: false, 57 | // minimum accepted FPS (suggested range 20 to 60) 58 | minFPS: 60, 59 | // maximum delay per each requestIdleCallback operation 60 | maxIdle: 2000, 61 | // remove a scheduled frame, idle, or timer operation 62 | clear: function (id) { 63 | return typeof id === 'number' ? 64 | clear(id) : 65 | void( 66 | drop(qframe, id) || 67 | drop(qidle, id) || 68 | drop(qframex, id) || 69 | drop(qidlex, id) 70 | ); 71 | }, 72 | // schedule a callback for the next frame 73 | // returns its unique id as object 74 | // .frame(callback[, arg0, arg1, argN]):object 75 | frame: function frame() { 76 | if (!frameRunning) { 77 | frameRunning = true; 78 | requestAnimationFrame(animationLoop); 79 | } 80 | return create.apply(qframe, arguments); 81 | }, 82 | // schedule a callback for the next idle callback 83 | // returns its unique id as object 84 | // .idle(callback[, arg0, arg1, argN]):object 85 | idle: function idle() { 86 | if (!idleRunning) { 87 | idleRunning = true; 88 | requestIdleCallback(idleLoop, {timeout: next.maxIdle}); 89 | } 90 | return create.apply(qidle, arguments); 91 | }, 92 | interval: createTimer(global.setInterval), 93 | timeout: createTimer(timeout), 94 | now: now 95 | }, 96 | // local variables 97 | // rAF and rIC states 98 | frameRunning = false, 99 | idleRunning = false, 100 | // animation frame and idle queues 101 | qframe = [], 102 | qidle = [], 103 | // animation frame and idle execution queues 104 | qframex = [], 105 | qidlex = [] 106 | ; 107 | 108 | // asliases 109 | next.raf = next.frame; 110 | next.ric = next.idle; 111 | 112 | switch (true) { 113 | case 'hidden' in document: 114 | hidden = 'hidden'; 115 | type = 'visibilitychange'; 116 | break; 117 | case 'msHidden' in document: 118 | hidden = 'msHidden'; 119 | type = 'msvisibilitychange'; 120 | break; 121 | case 'webkitHidden' in document: 122 | hidden = 'webkitHidden'; 123 | type = 'webkitvisibilitychange'; 124 | break; 125 | } 126 | 127 | if (hidden) { 128 | document.addEventListener(type, onVisibility, false); 129 | onVisibility(); 130 | } 131 | 132 | // responsible for centralized requestAnimationFrame operations 133 | function animationLoop() { 134 | var 135 | // grab current time 136 | t = time(), 137 | // calculate how many millisends we have 138 | fps = 1000 / next.minFPS, 139 | // used to flag overtime in case we exceed milliseconds 140 | overTime = false, 141 | // take current frame queue length 142 | length = getLength(qframe, qframex) 143 | ; 144 | // if there is actually something to do 145 | if (length) { 146 | // reschedule upfront next animation frame 147 | // this prevents the need for a try/catch within the while loop 148 | requestAnimationFrame(animationLoop); 149 | // reassign qframex cleaning current animation frame queue 150 | qframex = qframe.splice(0, length); 151 | while (qframex.length) { 152 | // if some of them fails, it's OK 153 | // next round will re-prioritize the animation frame queue 154 | exec(qframex.shift()); 155 | // if we exceeded the frame time, get out this loop 156 | overTime = (time() - t) >= fps; 157 | if ((next.isOverloaded = overTime)) break; 158 | } 159 | // if overtime and debug is true, warn about it 160 | if (overTime && next.debug) console.warn('overloaded frame'); 161 | } else { 162 | // all frame callbacks have been executed 163 | // we can actually stop asking for animation frames 164 | frameRunning = false; 165 | // and flag it as non busy/overloaded anymore 166 | next.isOverloaded = frameRunning; 167 | } 168 | } 169 | 170 | // create a unique id and returns it 171 | // if the callback with same extra arguments 172 | // was already scheduled, then returns same id 173 | function create(callback) { 174 | /* jslint validthis: true */ 175 | for (var 176 | queue = this, 177 | args = [], 178 | info = { 179 | id: {}, 180 | fn: callback, 181 | ar: args 182 | }, 183 | i = 1; i < arguments.length; i++ 184 | ) args[i - 1] = arguments[i]; 185 | return infoId(queue, info) || (queue.push(info), info.id); 186 | } 187 | 188 | // create setTimeout or setInterval wrapper 189 | function createTimer(timer) { 190 | return function (fn) { 191 | // overwrite the function with one that 192 | // execute only if the tab is visible 193 | // scheduling eventually for later and once 194 | // in case the tab is not 195 | arguments[0] = function () { 196 | if (tabIsVisible) fn.apply(null, arguments); 197 | else if (onceVisible.indexOf(fn) < 0) { 198 | onceVisible.push(fn, arguments); 199 | } 200 | }; 201 | return timer.apply(null, arguments); 202 | }; 203 | } 204 | 205 | // remove a scheduled id from a queue 206 | function drop(queue, id) { 207 | var 208 | i = findIndex(queue, id), 209 | found = -1 < i 210 | ; 211 | if (found) queue.splice(i, 1); 212 | return found; 213 | } 214 | 215 | // execute a shceduled callback with optional args 216 | function exec(info) { 217 | info.fn.apply(null, info.ar); 218 | } 219 | 220 | // find queue index by id 221 | function findIndex(queue, id) { 222 | var i = queue.length; 223 | while (i-- && queue[i].id !== id); 224 | return i; 225 | } 226 | 227 | // return the right queue length to consider 228 | // re-prioritizing scheduled callbacks 229 | function getLength(queue, queuex) { 230 | // if previous call didn't execute all callbacks 231 | return queuex.length ? 232 | // reprioritize the queue putting those in front 233 | queue.unshift.apply(queue, queuex) : 234 | queue.length; 235 | } 236 | 237 | // responsible for centralized requestIdleCallback operations 238 | function idleLoop(deadline) { 239 | var 240 | length = getLength(qidle, qidlex), 241 | didTimeout = deadline.didTimeout 242 | ; 243 | if (length) { 244 | // reschedule upfront next idle callback 245 | requestIdleCallback(idleLoop, {timeout: next.maxIdle}); 246 | // this prevents the need for a try/catch within the while loop 247 | // reassign qidlex cleaning current idle queue 248 | qidlex = qidle.splice(0, didTimeout ? 1 : length); 249 | while (qidlex.length && (didTimeout || deadline.timeRemaining())) 250 | exec(qidlex.shift()); 251 | } else { 252 | // all idle callbacks have been executed 253 | // we can actually stop asking for idle operations 254 | idleRunning = false; 255 | } 256 | } 257 | 258 | // return a scheduled unique id through similar info 259 | function infoId(queue, info) { 260 | for (var i = 0, length = queue.length, tmp; i < length; i++) { 261 | tmp = queue[i]; 262 | if ( 263 | tmp.fn === info.fn && 264 | sameValues(tmp.ar, info.ar) 265 | ) return tmp.id; 266 | } 267 | return null; 268 | } 269 | 270 | function onVisibility() { 271 | tabIsVisible = !document[hidden]; 272 | for (var 273 | length = onceVisible.length, 274 | list = onceVisible.splice(0, length), 275 | i = 0; i < length; i += 2 276 | ) { 277 | list[i].apply(null, list[i + 1]); 278 | } 279 | } 280 | 281 | // compare two arrays values 282 | function sameValues(a, b) { 283 | var 284 | i = a.length, 285 | j = b.length, 286 | k = i === j 287 | ; 288 | if (k) { 289 | while (i--) { 290 | if (a[i] !== b[i]) { 291 | return !k; 292 | } 293 | } 294 | } 295 | return k; 296 | } 297 | 298 | // return performance.now() real or sham value 299 | function time() { 300 | return now.call(performance); 301 | } 302 | -------------------------------------------------------------------------------- /test/sob.js: -------------------------------------------------------------------------------- 1 | //remove: 2 | var next = require('../build/sob.node.js'); 3 | //:remove 4 | 5 | wru.test([ 6 | { 7 | name: "it's an obejct", 8 | test: function () { 9 | wru.assert(typeof sob == 'object'); 10 | } 11 | }, { 12 | name: 'requestAnimationFrame', 13 | test: function () { 14 | sob.frame(wru.async(function () { 15 | wru.assert('requestAnimationFrame happened', true); 16 | })); 17 | } 18 | }, { 19 | name: 'requestIdleCallback', 20 | test: function () { 21 | sob.frame(wru.async(function () { 22 | wru.assert('requestIdleCallback happened', true); 23 | })); 24 | } 25 | }, { 26 | name: 'clear', 27 | test: function () { 28 | var 29 | animHappened = false, 30 | idleHappened = false, 31 | animId = sob.frame(function () { 32 | animHappened = true; 33 | }), 34 | idleId = sob.idle(function () { 35 | idleHappened = true; 36 | }) 37 | ; 38 | sob.clear(animId); 39 | sob.clear(idleId); 40 | setTimeout(wru.async(function () { 41 | wru.assert(!animHappened && !idleHappened); 42 | }), 300); 43 | } 44 | }, { 45 | name: 'clear inverted', 46 | test: function () { 47 | var 48 | animHappened = false, 49 | idleHappened = false, 50 | animId = sob.frame(function () { 51 | animHappened = true; 52 | }), 53 | idleId = sob.idle(function () { 54 | idleHappened = true; 55 | }) 56 | ; 57 | sob.clear(idleId); 58 | sob.clear(animId); 59 | setTimeout(wru.async(function () { 60 | wru.assert(!animHappened && !idleHappened); 61 | }), 300); 62 | } 63 | }, { 64 | name: 'no duplicated rAF', 65 | test: function () { 66 | var 67 | executed = [], 68 | first = function () { 69 | executed.push(first); 70 | }, 71 | second = function () { 72 | executed.push(second); 73 | } 74 | ; 75 | sob.frame(first); 76 | sob.frame(first); 77 | sob.frame(second); 78 | sob.frame(first); 79 | sob.frame(second); 80 | sob.frame(second); 81 | sob.frame(first); 82 | setTimeout(wru.async(function () { 83 | wru.assert( 84 | executed.length === 2 && 85 | executed[0] === first && 86 | executed[1] === second 87 | ); 88 | }), 300); 89 | } 90 | }, { 91 | name: 'no duplicated rIC', 92 | test: function () { 93 | var 94 | executed = [], 95 | first = function () { 96 | executed.push(first); 97 | }, 98 | second = function () { 99 | executed.push(second); 100 | } 101 | ; 102 | sob.idle(first); 103 | sob.idle(first); 104 | sob.idle(second); 105 | sob.idle(first); 106 | sob.idle(second); 107 | sob.idle(second); 108 | sob.idle(first); 109 | setTimeout(wru.async(function () { 110 | wru.assert( 111 | executed.length === 2 && 112 | executed[0] === first && 113 | executed[1] === second 114 | ); 115 | }), 300); 116 | } 117 | }, { 118 | name: 'passed arguments rAF', 119 | test: function () { 120 | var 121 | executed = [], 122 | first = function () { 123 | executed.push(arguments); 124 | }, 125 | second = function () { 126 | executed.push(arguments); 127 | } 128 | ; 129 | sob.frame(first, 1, 2, 3); 130 | sob.frame(second, 4, 5, 6); 131 | setTimeout(wru.async(function () { 132 | wru.assert( 133 | [].join.call(executed[0]) === '1,2,3' && 134 | [].join.call(executed[1]) === '4,5,6' 135 | ); 136 | }), 300); 137 | } 138 | }, { 139 | name: 'passed arguments rIC', 140 | test: function () { 141 | var 142 | executed = [], 143 | first = function () { 144 | executed.push(arguments); 145 | }, 146 | second = function () { 147 | executed.push(arguments); 148 | } 149 | ; 150 | sob.idle(first, 1, 2, 3); 151 | sob.idle(second, 4, 5); 152 | setTimeout(wru.async(function () { 153 | wru.assert( 154 | [].join.call(executed[0]) === '1,2,3' && 155 | [].join.call(executed[1]) === '4,5' 156 | ); 157 | }), 300); 158 | } 159 | }, { 160 | name: 'no duplicated rAF + args', 161 | test: function () { 162 | var 163 | executed = [], 164 | first = function first() { 165 | executed.push({ 166 | fn: first, 167 | ar: [].slice.call(arguments) 168 | }); 169 | }, 170 | second = function second() { 171 | executed.push({ 172 | fn: second, 173 | ar: [].slice.call(arguments) 174 | }); 175 | } 176 | ; 177 | sob.frame(first, 1, 2, 3); 178 | sob.frame(first, 1, 2, 3); 179 | sob.frame(second, 4, 5); 180 | sob.frame(first, 1, 2, 3); 181 | sob.frame(second, 4, 5); 182 | sob.frame(second, 4, 5); 183 | sob.frame(first, 1, 2, 3); 184 | setTimeout(wru.async(function () { 185 | wru.assert( 186 | executed.length === 2 && 187 | executed[0].fn === first && 188 | executed[1].fn === second && 189 | executed[0].ar.join(',') === '1,2,3' && 190 | executed[1].ar.join(',') === '4,5' 191 | ); 192 | }), 300); 193 | } 194 | }, { 195 | name: 'no duplicated rIC + args', 196 | test: function () { 197 | var 198 | executed = [], 199 | first = function first() { 200 | executed.push({ 201 | fn: first, 202 | ar: [].slice.call(arguments) 203 | }); 204 | }, 205 | second = function second() { 206 | executed.push({ 207 | fn: second, 208 | ar: [].slice.call(arguments) 209 | }); 210 | } 211 | ; 212 | sob.idle(first, 1, 2, 3); 213 | sob.idle(first, 1, 2, 3); 214 | sob.idle(second, 4, 5); 215 | sob.idle(first, 1, 2, 3); 216 | sob.idle(second, 4, 5); 217 | sob.idle(second, 4, 5); 218 | sob.idle(first, 1, 2, 3); 219 | setTimeout(wru.async(function () { 220 | wru.assert( 221 | executed.length === 2 && 222 | executed[0].fn === first && 223 | executed[1].fn === second && 224 | executed[0].ar.join(',') === '1,2,3' && 225 | executed[1].ar.join(',') === '4,5' 226 | ); 227 | }), 300); 228 | } 229 | }, { 230 | name: 'same callback, different rAF args', 231 | test: function () { 232 | var 233 | executed = [], 234 | first = function () { 235 | executed.push({ 236 | fn: first, 237 | ar: [].slice.call(arguments) 238 | }); 239 | }, 240 | second = function () { 241 | executed.push({ 242 | fn: second, 243 | ar: [].slice.call(arguments) 244 | }); 245 | }, 246 | ids = [ 247 | sob.frame(first), 248 | sob.frame(first, 1), 249 | sob.frame(first, 1, 2), 250 | sob.frame(second), 251 | sob.frame(second, 1), 252 | sob.frame(first), 253 | sob.frame(first, 1), 254 | sob.frame(first, 1, 2), 255 | sob.frame(second), 256 | sob.frame(second, 1) 257 | ] 258 | ; 259 | sob.clear(ids[1]); 260 | sob.clear(ids[3]); 261 | setTimeout(wru.async(function () { 262 | wru.assert( 263 | executed.length === 3 && 264 | executed[0].fn === first && 265 | executed[1].fn === first && 266 | executed[2].fn === second && 267 | executed[0].ar.join(',') === '' && 268 | executed[1].ar.join(',') === '1,2' && 269 | executed[2].ar.join(',') === '1' 270 | ); 271 | }), 300); 272 | } 273 | }, { 274 | name: 'same callback, different rIC args', 275 | test: function () { 276 | var 277 | executed = [], 278 | first = function () { 279 | executed.push({ 280 | fn: first, 281 | ar: [].slice.call(arguments) 282 | }); 283 | }, 284 | second = function () { 285 | executed.push({ 286 | fn: second, 287 | ar: [].slice.call(arguments) 288 | }); 289 | }, 290 | ids = [ 291 | sob.idle(first), 292 | sob.idle(first, 1), 293 | sob.idle(first, 1, 2), 294 | sob.idle(second), 295 | sob.idle(second, 1), 296 | sob.idle(first), 297 | sob.idle(first, 1), 298 | sob.idle(first, 1, 2), 299 | sob.idle(second), 300 | sob.idle(second, 1) 301 | ] 302 | ; 303 | sob.clear(ids[1]); 304 | sob.clear(ids[3]); 305 | setTimeout(wru.async(function () { 306 | wru.assert( 307 | executed.length === 3 && 308 | executed[0].fn === first && 309 | executed[1].fn === first && 310 | executed[2].fn === second && 311 | executed[0].ar.join(',') === '' && 312 | executed[1].ar.join(',') === '1,2' && 313 | executed[2].ar.join(',') === '1' 314 | ); 315 | }), 300); 316 | } 317 | }, { 318 | name: 'overload', 319 | test: function () { 320 | sob.frame(function () { 321 | var t = (new Date).getTime(); 322 | while ((new Date).getTime() - t < (1000 / (sob.minFPS / 2))); 323 | }); 324 | sob.frame(wru.async(function () { 325 | wru.assert(sob.isOverloaded); 326 | })); 327 | } 328 | }, { 329 | name: 'multiple calls', 330 | test: function () { 331 | var 332 | frame = 0, 333 | idle = 0, 334 | done = wru.async(function () { 335 | wru.assert(true); 336 | }) 337 | ; 338 | (function f() { 339 | if (++frame >= 10 && idle === 10) done(); 340 | else sob.frame(f); 341 | }()); 342 | (function i() { 343 | if (++idle < 10) sob.idle(i); 344 | }()); 345 | } 346 | }, { 347 | name: 'fail safe frame', 348 | test: function () { 349 | var calls = []; 350 | sob.frame(function a() { calls.push('a'); }); 351 | sob.frame(function b() { throw new Error('b'); }); 352 | sob.frame(function c() { calls.push('c'); }); 353 | sob.frame(function d() { calls.push('d'); }); 354 | sob.frame(wru.async(function () { 355 | wru.assert(calls.join(',') === 'a,c,d'); 356 | })); 357 | } 358 | }, { 359 | name: 'fail safe idle', 360 | test: function () { 361 | var calls = []; 362 | sob.idle(function a() { calls.push('a'); }); 363 | sob.idle(function b() { throw new Error('b'); }); 364 | sob.idle(function c() { calls.push('c'); }); 365 | sob.idle(function d() { calls.push('d'); }); 366 | sob.idle(wru.async(function () { 367 | wru.assert(calls.join(',') === 'a,c,d'); 368 | })); 369 | } 370 | }, { 371 | name: 'performance', 372 | test: function () { 373 | var 374 | counter = 0, 375 | id 376 | ; 377 | setTimeout(wru.async(function () { 378 | sob.clear(id); 379 | wru.assert('frames ' + counter, sob.minFPS <= counter); 380 | }), 1000); 381 | (function run() { 382 | counter++; 383 | id = sob.frame(run); 384 | }()); 385 | } 386 | }, { 387 | name: 'visibility dependent', 388 | test: function () { 389 | setTimeout(wru.async(function () { 390 | wru.assert('counter incremented only 3 times', counter === 3); 391 | }), 300); 392 | var counter = 0, i = sob.interval(function () { 393 | counter++; 394 | }, 70); 395 | sob.timeout(function () { 396 | counter++; 397 | sob.clear(i); 398 | i = sob.timeout(function () { 399 | counter++; 400 | }, 0); 401 | sob.clear(i); 402 | }, 180); 403 | } 404 | } 405 | ]); 406 | --------------------------------------------------------------------------------