Use a scheduler to schedule animation frames.
This commit is contained in:
Родитель
19de443639
Коммит
0091c624b1
119
src/index.js
119
src/index.js
|
@ -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()
|
||||
|
|
Загрузка…
Ссылка в новой задаче