UNPKG

@dark-engine/core

Version:

The lightweight and powerful UI rendering engine without dependencies and written in TypeScript (Browser, Node.js, Android, iOS, Windows, Linux, macOS)

316 lines (315 loc) 7.96 kB
import { getTime, detectIsPromise, detectIsFunction, dummyFn } from '../utils'; import { HOOK_DELIMETER, YIELD_INTERVAL, TaskPriority } from '../constants'; import { workLoop, detectIsBusy } from '../workloop'; import { EventEmitter } from '../emitter'; import { platform } from '../platform'; class MessageChannel extends EventEmitter { port1 = null; port2 = null; constructor() { super(); this.port1 = new MessagePort(this); this.port2 = new MessagePort(this); } } class MessagePort { channel; offs = []; constructor(channel) { this.channel = channel; } on(event, callback) { const off = this.channel.on(event, callback); this.offs.push(off); } postMessage(value) { platform.spawn(() => { this.channel.emit('message', value); }); } unref() { this.offs.forEach(x => x()); this.offs = []; } } class Scheduler { queue = { [TaskPriority.HIGH]: [], [TaskPriority.NORMAL]: [], [TaskPriority.LOW]: [], }; batched = []; deadline = 0; lastId = 0; task = null; scheduledCallback = null; isMessageLoopRunning = false; channel = null; port = null; constructor() { this.channel = new MessageChannel(); this.port = this.channel.port2; this.channel.port1.on('message', this.performWorkUntilDeadline.bind(this)); } reset() { this.deadline = 0; this.task = null; this.scheduledCallback = null; this.isMessageLoopRunning = false; } shouldYield() { return getTime() >= this.deadline; } schedule(callback, options) { const task = createTask(callback, options); if (options.isBatch) { const { setupBatch: setup } = options; this.batched.push({ task, setup }); return; } this.putAndExecute(task); } putAndExecute(task) { this.lastId = task.getId(); this.put(task); this.execute(); } batch() { const { batched } = this; const size = batched.length; for (let i = 0; i < size; i++) { const { task, setup } = batched[i]; if (i < size - 1) { detectIsFunction(setup) && setup(); } else { this.putAndExecute(task); } } this.batched = []; } getLastId() { return this.lastId; } detectIsTransition() { return this.task.getIsTransition(); } hasNewTask() { const { high, normal, low } = this.getQueues(); return high.length + normal.length + low.length > 0; } retain(fn) { const { high, normal, low } = this.getQueues(); const tasks = [...high, ...normal, ...low]; const { hasHostUpdate, hasChildUpdate } = collectFlags(this.task, tasks); if (hasHostUpdate || hasChildUpdate) { const hasExact = detectHasExact(this.task, tasks); if (hasExact) { this.complete(this.task, true); } else { this.defer(this.task); } this.task.markAsObsolete(); } else { this.task.setOnRestore(fn); this.defer(this.task); } } complete(task, isCanceled) { task.complete(isCanceled); } put(task) { const queue = this.queue[task.getPriority()]; if (task.getIsTransition()) { const base = task.base(); const tasks = queue.filter(x => x.base() !== base); queue.splice(0, queue.length, ...tasks); } queue.push(task); } pick(queue) { if (queue.length === 0) return false; this.task = queue.shift(); this.run(this.task); return true; } run(task) { try { task.run(); task.getForceAsync() ? this.requestCallbackAsync(workLoop) : this.requestCallback(workLoop); } catch (something) { if (detectIsPromise(something)) { something.catch(dummyFn).finally(() => { this.run(task); }); } else { throw something; } } } execute() { const isBusy = detectIsBusy(); if (!isBusy && !this.isMessageLoopRunning) { const { high, normal, low } = this.getQueues(); this.pick(high) || this.pick(normal) || this.pick(low); } } requestCallbackAsync(callback) { this.scheduledCallback = callback; if (!this.isMessageLoopRunning) { this.isMessageLoopRunning = true; this.port.postMessage(null); } } requestCallback(callback) { const something = callback(false); if (detectIsPromise(something)) { something.catch(dummyFn).finally(() => { this.requestCallback(callback); }); } else { this.task = null; this.execute(); } } performWorkUntilDeadline() { if (this.scheduledCallback) { this.deadline = getTime() + YIELD_INTERVAL; const something = this.scheduledCallback(true); if (detectIsPromise(something)) { something.catch(dummyFn).finally(() => { this.port.postMessage(null); }); } else if (something) { this.port.postMessage(null); } else { this.complete(this.task, false); this.reset(); this.execute(); } } else { this.isMessageLoopRunning = false; } } defer(task) { const { low } = this.getQueues(); low.unshift(task); } getQueues() { const high = this.queue[TaskPriority.HIGH]; const normal = this.queue[TaskPriority.NORMAL]; const low = this.queue[TaskPriority.LOW]; return { high, normal, low, }; } } class Task { __id; priority; forceAsync = false; isTransition = false; isObsolete = false; callback = null; createLoc = null; onRestore = null; onTransitionEnd = null; static nextTaskId = 0; constructor(callback, priority, forceAsync) { this.__id = ++Task.nextTaskId; this.callback = callback; this.priority = priority; this.forceAsync = forceAsync; } getId() { return this.__id; } getPriority() { return this.priority; } getForceAsync() { return this.forceAsync; } setIsTransition(x) { this.isTransition = x; } getIsTransition() { return this.isTransition; } run() { this.isObsolete = false; this.callback(this.onRestore); this.onRestore = null; } complete(isCanceled) { this.isTransition && !this.isObsolete && detectIsFunction(this.onTransitionEnd) && this.onTransitionEnd(loc => (isCanceled ? this.createBase(loc) === this.base() : false)); } markAsObsolete() { this.isObsolete = true; } getIsObsolete() { return this.isObsolete; } setOnRestore(fn) { this.onRestore = fn; } setCreateLoc(fn) { this.createLoc = fn; } createBase(loc) { const [base] = loc.split(HOOK_DELIMETER); return base; } base() { return this.createBase(this.loc()); } loc() { return this.createLoc(); } setOnTransitionEnd(fn) { this.onTransitionEnd = fn; } } function collectFlags(task, tasks) { const base = task.base(); let hasTopUpdate = false; let hasHostUpdate = false; let hasChildUpdate = false; for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; const $base = task.base(); if ($base.length < base.length && base.indexOf($base) === 0) { hasTopUpdate = true; } else if ($base === base) { hasHostUpdate = true; } else if ($base.length > base.length && $base.indexOf(base) === 0) { hasChildUpdate = true; } } return { hasTopUpdate, hasHostUpdate, hasChildUpdate, }; } function detectHasExact(task, tasks) { const $loc = task.loc(); const hasExact = tasks.some(x => x.loc() === $loc); return hasExact; } function createTask(callback, options) { const { priority = TaskPriority.NORMAL, forceAsync = false, isTransition = false, loc, onTransitionEnd } = options; const task = new Task(callback, priority, forceAsync); task.setIsTransition(isTransition); task.setOnTransitionEnd(onTransitionEnd); task.setCreateLoc(loc || rootLoc); return task; } const rootLoc = () => '>'; const scheduler = new Scheduler(); export { scheduler }; //# sourceMappingURL=scheduler.js.map