@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
189 lines (168 loc) • 4.83 kB
text/typescript
const SCHEDULE_MODE = {
idle: 'idle',
raf: 'raf',
timeout: 'timeout',
} as const
type ScheduleMode = (typeof SCHEDULE_MODE)[keyof typeof SCHEDULE_MODE]
export class JobQueue {
private isFlushing = false
private isFlushPending = false
private scheduleId = 0
private queue: Job[] = []
private frameInterval = 16
private initialTime = Date.now()
private pendingJobs = new Map<string, Job>()
private scheduleMode: ScheduleMode | null = null
queueJob(job: Job) {
if (job.priority & JOB_PRIORITY.PRIOR) {
job.cb()
} else {
const existing = this.pendingJobs.get(job.id)
if (existing) {
// 仅更新已有任务的回调与优先级
existing.cb = job.cb
if (job.priority !== existing.priority) {
existing.priority = job.priority
const idx = this.queue.indexOf(existing)
if (idx >= 0) {
this.queue.splice(idx, 1)
const newIndex = this.findInsertionIndex(existing)
this.queue.splice(newIndex, 0, existing)
}
}
} else {
const index = this.findInsertionIndex(job)
this.queue.splice(index, 0, job)
this.pendingJobs.set(job.id, job)
}
}
}
queueFlush() {
if (!this.isFlushing && !this.isFlushPending) {
this.isFlushPending = true
this.scheduleJob()
}
}
queueFlushSync() {
if (!this.isFlushing && !this.isFlushPending) {
this.isFlushPending = true
this.flushJobsSync()
}
}
clearJobs() {
this.queue.length = 0
this.pendingJobs.clear()
this.isFlushing = false
this.isFlushPending = false
this.cancelScheduleJob()
}
flushJobs(deadline?: IdleDeadline) {
this.isFlushPending = false
this.isFlushing = true
const startTime = this.getCurrentTime()
let budget = this.frameInterval
if (deadline && typeof deadline.timeRemaining === 'function') {
const remain = deadline.timeRemaining()
// 防止过长占用单帧
budget = Math.max(0, Math.min(budget, remain))
}
while (this.queue.length > 0) {
const job = this.queue.shift()!
job.cb()
this.pendingJobs.delete(job.id)
if (this.getCurrentTime() - startTime >= budget) {
break
}
}
this.isFlushing = false
if (this.queue.length) {
this.queueFlush()
}
}
flushJobsSync() {
this.isFlushPending = false
this.isFlushing = true
while (this.queue.length > 0) {
const job = this.queue.shift()!
try {
job.cb()
} catch (error) {
console.error(error)
}
this.pendingJobs.delete(job.id)
}
this.isFlushing = false
}
private findInsertionIndex(job: Job) {
let left = 0
let ins = this.queue.length
let right = ins - 1
const priority = job.priority
while (left <= right) {
const mid = ((right - left) >> 1) + left
if (priority <= this.queue[mid].priority) {
left = mid + 1
} else {
ins = mid
right = mid - 1
}
}
return ins
}
private scheduleJob() {
if (this.scheduleId) {
this.cancelScheduleJob()
}
if ('requestAnimationFrame' in window) {
this.scheduleMode = SCHEDULE_MODE.raf
this.scheduleId = (window as Window).requestAnimationFrame(() =>
this.flushJobs(),
)
} else if ('requestIdleCallback' in window) {
this.scheduleMode = SCHEDULE_MODE.idle
this.scheduleId = (window as Window).requestIdleCallback(
(deadline: IdleDeadline) => this.flushJobs(deadline),
{
timeout: 100,
},
)
} else {
this.scheduleMode = SCHEDULE_MODE.timeout
this.scheduleId = (window as Window).setTimeout(() => this.flushJobs())
}
}
private cancelScheduleJob() {
if (!this.scheduleId) return
const cancelMethods: Partial<Record<ScheduleMode, (id: number) => void>> = {
[SCHEDULE_MODE.idle]: window?.cancelIdleCallback,
[SCHEDULE_MODE.raf]: window?.cancelAnimationFrame,
[SCHEDULE_MODE.timeout]: window?.clearTimeout,
}
const mode = this.scheduleMode
const cancelMethod = mode ? cancelMethods[mode] : undefined
if (typeof cancelMethod === 'function') {
cancelMethod(this.scheduleId as number)
}
this.scheduleId = 0
this.scheduleMode = null
}
private getCurrentTime() {
const hasPerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function'
if (hasPerformanceNow) {
return performance.now()
}
return Date.now() - this.initialTime
}
}
export interface Job {
id: string
priority: JOB_PRIORITY
cb: () => void
}
export enum JOB_PRIORITY {
Update = /* */ 1 << 1,
RenderEdge = /**/ 1 << 2,
RenderNode = /**/ 1 << 3,
PRIOR = /* */ 1 << 20,
}