Use a scheduler to schedule animation frames.

This commit is contained in:
Irakli Gozalishvili 2016-03-01 15:11:13 -08:00
Родитель 19de443639
Коммит 0091c624b1
1 изменённых файлов: 94 добавлений и 25 удалений

Просмотреть файл

@ -13,17 +13,92 @@ import * as Signal from "reflex/type/signal"
import * as DOM from "reflex/type/dom"
*/
// Invariants:
// 1. In the NO_REQUEST state, there is never a scheduled animation frame.
// 2. In the PENDING_REQUEST and EXTRA_REQUEST states, there is always exactly
// one scheduled animation frame.
const NO_REQUEST = 0
const PENDING_REQUEST = 1
const EXTRA_REQUEST = 2
/*::
type Time = number
type State = 0 | 1 | 2
*/
class AnimationScheduler {
/*::
state: State;
requests: Array<(time:Time) => any>;
execute: (time:Time) => void;
*/
constructor() {
this.state = NO_REQUEST
this.requests = []
this.execute = this.execute.bind(this)
}
schedule(request) {
if (this.requests.indexOf(request) === -1) {
if (this.state === NO_REQUEST) {
window.requestAnimationFrame(this.execute)
}
this.requests.push(request)
this.state = PENDING_REQUEST
}
}
execute(time/*:Time*/)/*:void*/ {
switch (this.state) {
case NO_REQUEST:
// This state should not be possible. How can there be no
// request, yet somehow we are actively fulfilling a
// request?
throw Error(`Unexpected frame request`)
case PENDING_REQUEST:
// At this point, we do not *know* that another frame is
// needed, but we make an extra frame request just in
// case. It's possible to drop a frame if frame is requested
// too late, so we just do it preemptively.
window.requestAnimationFrame(this.execute)
this.state = EXTRA_REQUEST
this.dispatch(this.requests.splice(0), 0, time)
break
case EXTRA_REQUEST:
// Turns out the extra request was not needed, so we will
// stop requesting. No reason to call it all the time if
// no one needs it.
this.state = NO_REQUEST
break
}
}
dispatch(requests, index, time) {
const count = requests.length
try {
while (index < count) {
const request = requests[index]
index = index + 1
request(time)
}
} finally {
if (index < count) {
this.dispatch(requests, index, time)
}
}
}
}
const animationScheduler = new AnimationScheduler()
export class Renderer {
/*::
target: Element;
mount: ?(Element & {reflexTree?: DOM.VirtualTree});
value: Driver.VirtualRoot;
isScheduled: boolean;
state: number;
version: number;
address: Signal.Address<Driver.VirtualRoot>;
execute: () => void;
timeGroupName: ?string;
execute: (time:Time) => void;
render: Driver.render;
node: Driver.node;
thunk: Driver.thunk;
@ -31,15 +106,24 @@ export class Renderer {
text: ?Driver.text;
*/
constructor({target, timeGroupName}/*:{target: Element, timeGroupName?:string}*/) {
this.state = NO_REQUEST
this.target = target
this.mount = (target.children.length === 1 &&
target.children[0].reflexTree != null) ?
target.children[0] :
null
this.mount =
( target.children.length !== 1
? null
: target.children[0].reflexTree == null
? null
: target.children[0]
)
this.address = this.receive.bind(this)
this.execute = this.execute.bind(this)
this.timeGroupName = timeGroupName == null ? null : timeGroupName
this.timeGroupName =
( timeGroupName == null
? null
: timeGroupName
)
this.node = node
this.thunk = thunk
@ -49,13 +133,9 @@ export class Renderer {
return `Renderer({target: ${this.target}})`
}
receive(value/*:Driver.VirtualRoot*/) {
this.value = value
this.schedule()
}
schedule() {
if (!this.isScheduled) {
this.isScheduled = true
this.version = requestAnimationFrame(this.execute)
if (this.value !== value) {
this.value = value
animationScheduler.schedule(this.execute)
}
}
execute(_/*:number*/) {
@ -66,17 +146,6 @@ export class Renderer {
const start = performance.now()
// It is important to mark `isScheduled` as `false` before doing actual
// rendering since state changes in effect of reflecting current state
// won't be handled by this render cycle. For example rendering a state
// with updated focus will cause `blur` & `focus` events to be dispatched
// that happen synchronously, and there for another render cycle may be
// scheduled for which `isScheduled` must be `false`. Attempt to render
// this state may also cause a runtime exception but even then we would
// rather attempt to render updated states that end up being blocked
// forever.
this.isScheduled = false
this.value.renderWith(this)
const end = performance.now()