Skip to content

Commit 3c0bddf

Browse files
author
Anatoly Ostrovsky
committed
Scope optimizations
1 parent 96e62b5 commit 3c0bddf

1 file changed

Lines changed: 134 additions & 21 deletions

File tree

src/core/scope/scope.ts

Lines changed: 134 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ type ForeignListenerRef = {
5858
_id: number;
5959
};
6060

61+
type ListenerScheduleFilter = (listeners: Listener[]) => Listener[];
62+
63+
type ScheduledListenerTask = {
64+
_kind: "listener";
65+
_listeners: Listener[];
66+
_target: Scope | ScopeProxy | undefined;
67+
_filter?: ListenerScheduleFilter;
68+
};
69+
70+
type ScheduledCallbackTask = {
71+
_kind: "callback";
72+
_callback: () => void;
73+
};
74+
75+
type ScheduledTask = ScheduledListenerTask | ScheduledCallbackTask;
76+
77+
type ListenerSchedulerState = {
78+
_queue: ScheduledTask[];
79+
_queued: boolean;
80+
_flushing: boolean;
81+
};
82+
6183
export interface ScopeEvent {
6284
targetScope: typeof Proxy<ng.Scope>;
6385
currentScope: typeof Proxy<ng.Scope> | null;
@@ -542,6 +564,9 @@ export class Scope {
542564
/** @internal */
543565
_ownedForeignListeners: ForeignListenerRef[];
544566

567+
/** @internal */
568+
_listenerScheduler: ListenerSchedulerState;
569+
545570
/**
546571
* Initializes the handler with the target object and a context.
547572
*
@@ -579,6 +604,12 @@ export class Scope {
579604

580605
this._ownedForeignListeners = [];
581606

607+
this._listenerScheduler = context?._listenerScheduler ?? {
608+
_queue: [],
609+
_queued: false,
610+
_flushing: false,
611+
};
612+
582613
this._propertyMap = {
583614
$apply: this.$apply.bind(this),
584615
$broadcast: this.$broadcast.bind(this),
@@ -1148,19 +1179,111 @@ export class Scope {
11481179
}
11491180
}
11501181

1182+
/** @internal Queues a shared scheduled task flush for this scope family. */
1183+
_enqueueScheduledTask(task: ScheduledTask): void {
1184+
const scheduler = this._listenerScheduler;
1185+
1186+
scheduler._queue.push(task);
1187+
1188+
if (!scheduler._queued && !scheduler._flushing) {
1189+
scheduler._queued = true;
1190+
1191+
queueMicrotask(() => {
1192+
this._flushScheduledTasks();
1193+
});
1194+
}
1195+
}
1196+
1197+
/** @internal Flushes queued listener and callback tasks in FIFO order. */
1198+
_flushScheduledTasks(): void {
1199+
const scheduler = this._listenerScheduler;
1200+
1201+
if (scheduler._flushing) {
1202+
return;
1203+
}
1204+
1205+
scheduler._queued = false;
1206+
scheduler._flushing = true;
1207+
1208+
let processed = 0;
1209+
1210+
try {
1211+
while (processed < scheduler._queue.length) {
1212+
const task = scheduler._queue[processed++];
1213+
1214+
if (task._kind === "callback") {
1215+
task._callback();
1216+
this._drainPostUpdateQueue();
1217+
1218+
continue;
1219+
}
1220+
1221+
const filteredListeners = task._filter
1222+
? task._filter(task._listeners)
1223+
: task._listeners;
1224+
1225+
for (let i = 0, l = filteredListeners.length; i < l; i++) {
1226+
this._notifyListener(filteredListeners[i], task._target);
1227+
this._drainPostUpdateQueue();
1228+
}
1229+
}
1230+
} finally {
1231+
if (processed > 0) {
1232+
scheduler._queue.splice(0, processed);
1233+
}
1234+
1235+
scheduler._flushing = false;
1236+
1237+
if (scheduler._queue.length > 0 && !scheduler._queued) {
1238+
scheduler._queued = true;
1239+
1240+
queueMicrotask(() => {
1241+
this._flushScheduledTasks();
1242+
});
1243+
}
1244+
}
1245+
}
1246+
1247+
/** @internal Drains post-update callbacks in FIFO order. */
1248+
_drainPostUpdateQueue(): void {
1249+
if ($postUpdateQueue.length === 0) {
1250+
return;
1251+
}
1252+
1253+
let index = 0;
1254+
1255+
while (index < $postUpdateQueue.length) {
1256+
$postUpdateQueue[index++]();
1257+
}
1258+
1259+
$postUpdateQueue.length = 0;
1260+
}
1261+
1262+
/** @internal Schedules a callback to run in the shared listener flush queue. */
1263+
_scheduleCallback(callback: () => void): void {
1264+
this._enqueueScheduledTask({
1265+
_kind: "callback",
1266+
_callback: callback,
1267+
});
1268+
}
1269+
11511270
/** @internal Queues listener notification for the next microtask, optionally filtering the list first. */
11521271
_scheduleListener(
11531272
listeners: Listener[],
1154-
filter?: (listeners: Listener[]) => Listener[],
1273+
filterOrTarget?: ListenerScheduleFilter | Scope | ScopeProxy,
11551274
): void {
1156-
const target = this.$target;
1157-
1158-
queueMicrotask(() => {
1159-
const filteredListeners = filter ? filter(listeners) : listeners;
1160-
1161-
for (let i = 0, l = filteredListeners.length; i < l; i++) {
1162-
this._notifyListener(filteredListeners[i], target);
1163-
}
1275+
const filter =
1276+
typeof filterOrTarget === "function"
1277+
? (filterOrTarget as ListenerScheduleFilter)
1278+
: undefined;
1279+
1280+
const target = filter ? this.$target : (filterOrTarget ?? this.$target);
1281+
1282+
this._enqueueScheduledTask({
1283+
_kind: "listener",
1284+
_listeners: listeners,
1285+
_target: target,
1286+
_filter: filter,
11641287
});
11651288
}
11661289

@@ -1181,7 +1304,7 @@ export class Scope {
11811304
// Constant are immediately passed to listener function
11821305
if (get._constant) {
11831306
if (listenerFn) {
1184-
queueMicrotask(() => {
1307+
this._scheduleCallback(() => {
11851308
let res = get();
11861309

11871310
while (isFunction(res)) {
@@ -2023,24 +2146,14 @@ export class Scope {
20232146
}
20242147

20252148
_listenerFn(newVal, _originalTarget);
2026-
2027-
if ($postUpdateQueue.length > 0) {
2028-
for (let qi = 0; qi < $postUpdateQueue.length; qi++) {
2029-
$postUpdateQueue[qi]();
2030-
}
2031-
$postUpdateQueue.length = 0;
2032-
}
20332149
} catch (err) {
20342150
$exceptionHandler(err);
20352151
}
20362152
}
20372153

20382154
/* @ignore */
20392155
$flushQueue() {
2040-
for (let i = 0; i < $postUpdateQueue.length; i++) {
2041-
$postUpdateQueue[i]();
2042-
}
2043-
$postUpdateQueue.length = 0;
2156+
this._drainPostUpdateQueue();
20442157
}
20452158

20462159
/** Searches this scope tree for a scope with the given id. */

0 commit comments

Comments
 (0)