perf(raf): use rafFrames instead of fastdom

This commit is contained in:
Adam Bradley
2015-11-20 14:10:41 -06:00
parent e98a00a9f8
commit 19ff9e0a2c
8 changed files with 49 additions and 441 deletions

View File

@@ -1,6 +1,5 @@
import {CSS} from '../util/dom';
import {CSS, rafFrames} from '../util/dom';
import {extend} from '../util/util';
import {FastDom} from '../util/fastdom';
/**
@@ -25,9 +24,8 @@ import {FastDom} from '../util/fastdom';
export class Animation {
constructor(ele, opts={}, fastdom=null) {
constructor(ele, opts={}) {
this.reset();
this._fastdom = fastdom;
this._opts = extend({
renderDelay: 16
@@ -259,12 +257,7 @@ export class Animation {
if (self._duration > 16 && this._opts.renderDelay > 0) {
// begin each animation when everything is rendered in their starting point
// give the browser some time to render everything in place before starting
if (this._fastdom) {
this._fastdom.write(kickoff);
} else {
setTimeout(kickoff, this._opts.renderDelay);
}
rafFrames(this._opts.renderDelay / 16, kickoff);
} else {
// no need to render everything in there place before animating in

View File

@@ -1,5 +1,6 @@
import {Title} from 'angular2/angular2';
import {rafFrames} from '../../util/dom';
import {ClickBlock} from '../../util/click-block';
import {ScrollTo} from '../../animations/scroll-to';
@@ -10,9 +11,8 @@ import {ScrollTo} from '../../animations/scroll-to';
*/
export class IonicApp {
constructor(config, fastdom) {
constructor(config) {
this._config = config;
this._fastdom = fastdom;
this._titleSrv = new Title();
this._title = '';
this._disTime = 0;
@@ -29,7 +29,7 @@ export class IonicApp {
setTitle(val) {
if (val !== this._title) {
this._title = val;
this._fastdom.defer(4, () => {
rafFrames(4, () => {
this._titleSrv.setTitle(this._title);
});
}

View File

@@ -1,11 +1,10 @@
import {raf} from '../../util/dom';
import {raf, rafFrames} from '../../util/dom';
export class Activator {
constructor(app, config, fastdom) {
constructor(app, config) {
this.app = app;
this.fastdom = fastdom;
this.queue = [];
this.active = [];
this.clearStateDefers = 5;
@@ -27,7 +26,7 @@ export class Activator {
// queue to have this element activated
this.queue.push(activatableEle);
this.fastdom.write(() => {
rafFrames(2, () => {
let activatableEle;
for (let i = 0; i < this.queue.length; i++) {
activatableEle = this.queue[i];
@@ -44,7 +43,7 @@ export class Activator {
upAction() {
// the user was pressing down, then just let up
this.fastdom.defer(this.clearStateDefers, () => {
rafFrames(this.clearStateDefers, () => {
this.clearState();
});
}
@@ -52,17 +51,17 @@ export class Activator {
clearState() {
// all states should return to normal
if ((!this.app.isEnabled() || this.app.isTransitioning()) && this.clearAttempt < 100) {
if ((!this.app.isEnabled() || this.app.isTransitioning())) {
// the app is actively disabled, so don't bother deactivating anything.
// this makes it easier on the GPU so it doesn't have to redraw any
// buttons during a transition. This will retry in XX milliseconds.
++this.clearAttempt;
this.upAction();
setTimeout(() => {
this.clearState();
}, 600);
} else {
// not actively transitioning, good to deactivate any elements
this.deactivate();
this.clearAttempt = 0;
}
}
@@ -70,7 +69,7 @@ export class Activator {
// remove the active class from all active elements
this.queue = [];
this.fastdom.write(() => {
rafFrames(2, () => {
for (let i = 0; i < this.active.length; i++) {
this.active[i].classList.remove(this.activatedClass);
}

View File

@@ -1,12 +1,12 @@
import {Activator} from './activator';
import {Animation} from '../../animations/animation';
import {raf} from '../../util/dom';
import {raf, rafFrames} from '../../util/dom';
export class RippleActivator extends Activator {
constructor(app, config, fastdom) {
super(app, config, fastdom);
constructor(app, config) {
super(app, config);
this.expands = {};
this.fades = {};
@@ -18,16 +18,12 @@ export class RippleActivator extends Activator {
// create a new ripple element
this.expandSpeed = EXPAND_DOWN_PLAYBACK_RATE;
this.fastdom.defer(2, () => {
rafFrames(2, () => {
let clientRect = activatableEle.getBoundingClientRect();
this.fastdom.read(() => {
let clientRect = activatableEle.getBoundingClientRect();
this.fastdom.write(() => {
this.createRipple(activatableEle, pointerX, pointerY, clientRect);
});
raf(() => {
this.createRipple(activatableEle, pointerX, pointerY, clientRect);
});
});
}
}
@@ -60,7 +56,7 @@ export class RippleActivator extends Activator {
.duration(FADE_OUT_DURATION)
.playbackRate(1)
.onFinish(() => {
this.fastdom.write(() => {
raf(() => {
this.fades[rippleId].dispose(true);
delete this.fades[rippleId];
});
@@ -87,7 +83,7 @@ export class RippleActivator extends Activator {
this.expandSpeed = 1;
this.fastdom.defer(4, () => {
rafFrames(4, () => {
this.next();
});
}

View File

@@ -19,16 +19,16 @@ let doc = null;
/**
* @private
*/
export function initTapClick(windowInstance, documentInstance, appInstance, config, fastdom) {
export function initTapClick(windowInstance, documentInstance, appInstance, config) {
win = windowInstance;
doc = documentInstance;
app = appInstance;
if (config.get('activator') == 'ripple') {
activator = new RippleActivator(app, config, fastdom);
activator = new RippleActivator(app, config);
} else if (config.get('activator') == 'highlight') {
activator = new Activator(app, config, fastdom);
activator = new Activator(app, config);
}
isTapPolyfill = (config.get('tapPolyfill') === true);

View File

@@ -6,7 +6,6 @@ import {IonicApp} from '../components/app/app';
import {Config} from './config';
import {Platform} from '../platform/platform';
import {OverlayController} from '../components/overlay/overlay-controller';
import {FastDom} from '../util/fastdom';
import {Form} from '../util/form';
import {Keyboard} from '../util/keyboard';
import {ActionSheet} from '../components/action-sheet/action-sheet';
@@ -22,8 +21,6 @@ import * as dom from '../util/dom';
export function ionicProviders(args={}) {
let fastdom = new FastDom();
let platform = new Platform();
let navRegistry = new NavRegistry(args.pages);
@@ -39,10 +36,10 @@ export function ionicProviders(args={}) {
platform.load();
config.setPlatform(platform);
let app = new IonicApp(config, fastdom);
let app = new IonicApp(config);
let events = new Events();
initTapClick(window, document, app, config, fastdom);
initTapClick(window, document, app, config);
let featureDetect = new FeatureDetect();
setupDom(window, document, config, platform, featureDetect);
@@ -52,7 +49,6 @@ export function ionicProviders(args={}) {
platform.prepareReady(config);
return [
provide(FastDom, {useValue: fastdom}),
provide(IonicApp, {useValue: app}),
provide(Config, {useValue: config}),
provide(Platform, {useValue: platform}),

View File

@@ -7,15 +7,15 @@ const nativeCancelRaf = window.cancelAnimationFrame ||
window.webkitCancelRequestAnimationFrame;
export const raf = nativeRaf || function(callback) {
let timeCurrent = (new Date()).getTime(),
timeDelta;
let timeCurrent = (new Date()).getTime(),
timeDelta;
/* Dynamically set delay on a per-tick basis to match 60fps. */
/* Technique by Erik Moller. MIT license: https://gist.github.com/paulirish/1579671 */
timeDelta = Math.max(0, 16 - (timeCurrent - timeLast));
timeLast = timeCurrent + timeDelta;
/* Dynamically set delay on a per-tick basis to match 60fps. */
/* Technique by Erik Moller. MIT license: https://gist.github.com/paulirish/1579671 */
timeDelta = Math.max(0, 16 - (timeCurrent - timeLast));
timeLast = timeCurrent + timeDelta;
return setTimeout(function() { callback(timeCurrent + timeDelta); }, timeDelta);
return setTimeout(function() { callback(timeCurrent + timeDelta); }, timeDelta);
}
export const rafCancel = nativeRaf ? nativeCancelRaf : function(id) {
@@ -26,6 +26,19 @@ export function rafPromise() {
return new Promise(resolve => raf(resolve));
}
export function rafFrames(framesToWait, callback) {
framesToWait = Math.ceil(framesToWait);
if (framesToWait < 2) {
raf(callback);
} else {
setTimeout(() => {
raf(callback);
}, (framesToWait - 1) * 17);
}
}
export let CSS = {};
(function() {
// transform

View File

@@ -1,389 +0,0 @@
import {raf} from './dom';
/**
* FastDom
*
* Eliminates layout thrashing
* by batching DOM read/write
* interactions.
*
* @author Wilson Page <wilsonpage@me.com>
*/
/**
* Creates a fresh
* FastDom instance.
*
* @constructor
*/
export function FastDom() {
this.frames = [];
this.lastId = 0;
// Placing the rAF method
// on the instance allows
// us to replace it with
// a stub for testing.
this.raf = raf;
this.batch = {
hash: {},
read: [],
write: [],
mode: null
};
}
/**
* Adds a job to the
* read batch and schedules
* a new frame if need be.
*
* @param {Function} fn
* @public
*/
FastDom.prototype.read = function(fn, ctx) {
var job = this.add('read', fn, ctx);
var id = job.id;
// Add this job to the read queue
this.batch.read.push(job.id);
// We should *not* schedule a new frame if:
// 1. We're 'reading'
// 2. A frame is already scheduled
var doesntNeedFrame = this.batch.mode === 'reading'
|| this.batch.scheduled;
// If a frame isn't needed, return
if (doesntNeedFrame) return id;
// Schedule a new
// frame, then return
this.scheduleBatch();
return id;
};
/**
* Adds a job to the
* write batch and schedules
* a new frame if need be.
*
* @param {Function} fn
* @public
*/
FastDom.prototype.write = function(fn, ctx) {
var job = this.add('write', fn, ctx);
var mode = this.batch.mode;
var id = job.id;
// Push the job id into the queue
this.batch.write.push(job.id);
// We should *not* schedule a new frame if:
// 1. We are 'writing'
// 2. We are 'reading'
// 3. A frame is already scheduled.
var doesntNeedFrame = mode === 'writing'
|| mode === 'reading'
|| this.batch.scheduled;
// If a frame isn't needed, return
if (doesntNeedFrame) return id;
// Schedule a new
// frame, then return
this.scheduleBatch();
return id;
};
/**
* Defers the given job
* by the number of frames
* specified.
*
* If no frames are given
* then the job is run in
* the next free frame.
*
* @param {Number} frame
* @param {Function} fn
* @public
*/
FastDom.prototype.defer = function(frame, fn, ctx) {
// Accepts two arguments
if (typeof frame === 'function') {
ctx = fn;
fn = frame;
frame = 1;
}
var self = this;
var index = frame - 1;
return this.schedule(index, function() {
self.run({
fn: fn,
ctx: ctx
});
});
};
/**
* Clears a scheduled 'read',
* 'write' or 'defer' job.
*
* @param {Number|String} id
* @public
*/
FastDom.prototype.clear = function(id) {
// Defer jobs are cleared differently
if (typeof id === 'function') {
return this.clearFrame(id);
}
// Allow ids to be passed as strings
id = Number(id);
var job = this.batch.hash[id];
if (!job) return;
var list = this.batch[job.type];
var index = list.indexOf(id);
// Clear references
delete this.batch.hash[id];
if (~index) list.splice(index, 1);
};
/**
* Clears a scheduled frame.
*
* @param {Function} frame
* @private
*/
FastDom.prototype.clearFrame = function(frame) {
var index = this.frames.indexOf(frame);
if (~index) this.frames.splice(index, 1);
};
/**
* Schedules a new read/write
* batch if one isn't pending.
*
* @private
*/
FastDom.prototype.scheduleBatch = function() {
var self = this;
// Schedule batch for next frame
this.schedule(0, function() {
self.batch.scheduled = false;
self.runBatch();
});
// Set flag to indicate
// a frame has been scheduled
this.batch.scheduled = true;
};
/**
* Generates a unique
* id for a job.
*
* @return {Number}
* @private
*/
FastDom.prototype.uniqueId = function() {
return ++this.lastId;
};
/**
* Calls each job in
* the list passed.
*
* If a context has been
* stored on the function
* then it is used, else the
* current `this` is used.
*
* @param {Array} list
* @private
*/
FastDom.prototype.flush = function(list) {
var id;
while (id = list.shift()) {
this.run(this.batch.hash[id]);
}
};
/**
* Runs any 'read' jobs followed
* by any 'write' jobs.
*
* We run this inside a try catch
* so that if any jobs error, we
* are able to recover and continue
* to flush the batch until it's empty.
*
* @private
*/
FastDom.prototype.runBatch = function() {
try {
// Set the mode to 'reading',
// then empty all read jobs
this.batch.mode = 'reading';
this.flush(this.batch.read);
// Set the mode to 'writing'
// then empty all write jobs
this.batch.mode = 'writing';
this.flush(this.batch.write);
this.batch.mode = null;
} catch (e) {
this.runBatch();
throw e;
}
};
/**
* Adds a new job to
* the given batch.
*
* @param {Array} list
* @param {Function} fn
* @param {Object} ctx
* @returns {Number} id
* @private
*/
FastDom.prototype.add = function(type, fn, ctx) {
var id = this.uniqueId();
return this.batch.hash[id] = {
id: id,
fn: fn,
ctx: ctx,
type: type
};
};
/**
* Runs a given job.
*
* Applications using FastDom
* have the options of setting
* `fastdom.onError`.
*
* This will catch any
* errors that may throw
* inside callbacks, which
* is useful as often DOM
* nodes have been removed
* since a job was scheduled.
*
* Example:
*
* fastdom.onError = function(e) {
* // Runs when jobs error
* };
*
* @param {Object} job
* @private
*/
FastDom.prototype.run = function(job){
var ctx = job.ctx || this;
var fn = job.fn;
// Clear reference to the job
delete this.batch.hash[job.id];
// If no `onError` handler
// has been registered, just
// run the job normally.
if (!this.onError) {
return fn.call(ctx);
}
// If an `onError` handler
// has been registered, catch
// errors that throw inside
// callbacks, and run the
// handler instead.
try { fn.call(ctx); } catch (e) {
this.onError(e);
}
};
/**
* Starts a rAF loop
* to empty the frame queue.
*
* @private
*/
FastDom.prototype.loop = function() {
var self = this;
var raf = this.raf;
// Don't start more than one loop
if (this.looping) return;
raf(function frame() {
var fn = self.frames.shift();
// If no more frames,
// stop looping
if (!self.frames.length) {
self.looping = false;
// Otherwise, schedule the
// next frame
} else {
raf(frame);
}
// Run the frame. Note that
// this may throw an error
// in user code, but all
// fastdom tasks are dealt
// with already so the code
// will continue to iterate
if (fn) fn();
});
this.looping = true;
};
/**
* Adds a function to
* a specified index
* of the frame queue.
*
* @param {Number} index
* @param {Function} fn
* @return {Function}
* @private
*/
FastDom.prototype.schedule = function(index, fn) {
// Make sure this slot
// hasn't already been
// taken. If it has, try
// re-scheduling for the next slot
if (this.frames[index]) {
return this.schedule(index + 1, fn);
}
// Start the rAF
// loop to empty
// the frame queue
this.loop();
// Insert this function into
// the frames queue and return
return this.frames[index] = fn;
};