mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(zone.js): support addEventListener with signal option. (#49595)
Close #49591 ``` const ac = new AbortController(); addEventListener(eventName, handler, {signal: ac.signal);` ac.abort(); ``` Currently `zone.js` doesn't support the `signal` option, this PR allows the user to use AbortContoller to remove the event listener. PR Close #49595
This commit is contained in:
committed by
Jessica Janiuk
parent
8e5b95d746
commit
ded2bc9d99
@@ -293,6 +293,16 @@ export function patchEventTarget(
|
||||
// if task is not marked as isRemoved, this call is directly
|
||||
// from Zone.prototype.cancelTask, we should remove the task
|
||||
// from tasksList of target first
|
||||
const signal = task?.options?.signal;
|
||||
if (typeof signal === 'object' && signal?.tasks) {
|
||||
const abortTasks = signal.tasks;
|
||||
for (let i = 0; i < abortTasks?.length || 0; i++) {
|
||||
if (abortTasks[i] === task) {
|
||||
abortTasks.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!task.isRemoved) {
|
||||
const symbolEventNames = zoneSymbolEventNames[task.eventName];
|
||||
let symbolEventName;
|
||||
@@ -394,6 +404,11 @@ export function patchEventTarget(
|
||||
const passive =
|
||||
passiveSupported && !!passiveEvents && passiveEvents.indexOf(eventName) !== -1;
|
||||
const options = buildEventListenerOptions(arguments[2], passive);
|
||||
const signal = typeof options === 'object' && options?.signal;
|
||||
if (typeof signal === 'object' && signal?.aborted) {
|
||||
// the signal is an aborted one, just return without attaching the event listener.
|
||||
return;
|
||||
}
|
||||
|
||||
if (unpatchedEvents) {
|
||||
// check unpatched list
|
||||
@@ -465,9 +480,31 @@ export function patchEventTarget(
|
||||
(data as any).taskData = taskData;
|
||||
}
|
||||
|
||||
if (signal && typeof signal === 'object') {
|
||||
// if addEventListener with signal options, we don't pass it to
|
||||
// native addEventListener, instead we keep the signal setting
|
||||
// and handle ourselves.
|
||||
taskData.options.signal = undefined;
|
||||
}
|
||||
const task: any =
|
||||
zone.scheduleEventTask(source, delegate, data, customScheduleFn, customCancelFn);
|
||||
|
||||
if (signal && typeof signal === 'object') {
|
||||
// after task is scheduled, we need to store the signal back to task.options
|
||||
taskData.options.signal = signal;
|
||||
const tasks = signal.tasks || [];
|
||||
tasks.push(task);
|
||||
signal.tasks = tasks;
|
||||
if (!signal[Zone.__symbol__('abortListener')]) {
|
||||
signal[Zone.__symbol__('abortListener')] = true;
|
||||
nativeListener.call(signal, 'abort', function() {
|
||||
const sTasks = signal.tasks.slice();
|
||||
sTasks.forEach((task: Task) => task.zone.cancelTask(task));
|
||||
signal.tasks.length = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// should clear taskData.target to avoid memory leak
|
||||
// issue, https://github.com/angular/angular/issues/20442
|
||||
taskData.target = null;
|
||||
|
||||
@@ -29,23 +29,7 @@ Zone.__load_patch('fetch', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
|
||||
const fetchTaskAborting = api.symbol('fetchTaskAborting');
|
||||
const OriginalAbortController = global['AbortController'];
|
||||
const supportAbort = typeof OriginalAbortController === 'function';
|
||||
let abortNative: Function|null = null;
|
||||
if (supportAbort) {
|
||||
global['AbortController'] = function() {
|
||||
const abortController = new OriginalAbortController();
|
||||
const signal = abortController.signal;
|
||||
signal.abortController = abortController;
|
||||
return abortController;
|
||||
};
|
||||
abortNative = api.patchMethod(
|
||||
OriginalAbortController.prototype, 'abort',
|
||||
(delegate: Function) => (self: any, args: any) => {
|
||||
if (self.task) {
|
||||
return self.task.zone.cancelTask(self.task);
|
||||
}
|
||||
return delegate.apply(self, args);
|
||||
});
|
||||
}
|
||||
let abortNative: Function|null = OriginalAbortController?.prototype[api.symbol('abort')];
|
||||
const placeholder = function() {};
|
||||
global['fetch'] = function() {
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
@@ -105,7 +89,7 @@ Zone.__load_patch('fetch', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
|
||||
}
|
||||
});
|
||||
if (signal && signal.abortController) {
|
||||
signal.abortController.task = task;
|
||||
signal.abortController.tasks = [task];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1762,6 +1762,113 @@ describe('Zone', function() {
|
||||
expect(logs).toEqual(['click2']);
|
||||
});
|
||||
|
||||
it('should support remove event listeners via AbortController', function() {
|
||||
let logs: string[] = [];
|
||||
const ac = new AbortController();
|
||||
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click1');
|
||||
}, {signal: ac.signal});
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click2');
|
||||
});
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click3');
|
||||
}, {signal: ac.signal});
|
||||
let listeners = button.eventListeners!('click');
|
||||
expect(listeners.length).toBe(3);
|
||||
|
||||
button.dispatchEvent(clickEvent);
|
||||
expect(logs.length).toBe(3);
|
||||
expect(logs).toEqual(['click1', 'click2', 'click3']);
|
||||
ac.abort();
|
||||
logs = [];
|
||||
|
||||
listeners = button.eventListeners!('click');
|
||||
button.dispatchEvent(clickEvent);
|
||||
expect(logs.length).toBe(1);
|
||||
expect(listeners.length).toBe(1);
|
||||
expect(logs).toEqual(['click2']);
|
||||
});
|
||||
|
||||
it('should support remove event listeners with AbortController', function() {
|
||||
let logs: string[] = [];
|
||||
const ac = new AbortController();
|
||||
|
||||
const listener1 = function() {
|
||||
logs.push('click1');
|
||||
};
|
||||
button.addEventListener('click', listener1, {signal: ac.signal});
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click2');
|
||||
});
|
||||
let listeners = button.eventListeners!('click');
|
||||
expect(listeners.length).toBe(2);
|
||||
|
||||
button.dispatchEvent(clickEvent);
|
||||
expect(logs.length).toBe(2);
|
||||
expect(logs).toEqual(['click1', 'click2']);
|
||||
|
||||
button.removeEventListener('click', listener1);
|
||||
listeners = button.eventListeners!('click');
|
||||
expect(listeners.length).toBe(1);
|
||||
|
||||
logs = [];
|
||||
|
||||
listeners = button.eventListeners!('click');
|
||||
button.dispatchEvent(clickEvent);
|
||||
expect(logs.length).toBe(1);
|
||||
expect(listeners.length).toBe(1);
|
||||
expect(logs).toEqual(['click2']);
|
||||
|
||||
ac.abort();
|
||||
expect(logs).toEqual(['click2']);
|
||||
});
|
||||
|
||||
it('should not add event listeners with aborted signal', function() {
|
||||
let logs: string[] = [];
|
||||
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click1');
|
||||
}, {signal: AbortSignal.abort()});
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click2');
|
||||
});
|
||||
let listeners = button.eventListeners!('click');
|
||||
expect(listeners.length).toBe(1);
|
||||
|
||||
button.dispatchEvent(clickEvent);
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs).toEqual(['click2']);
|
||||
});
|
||||
|
||||
it('should remove event listeners with timeout signal',
|
||||
ifEnvSupportsWithDone(
|
||||
() => typeof AbortSignal.timeout === 'function', function(done: DoneFn) {
|
||||
let logs: string[] = [];
|
||||
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click1');
|
||||
}, {signal: AbortSignal.timeout(1)});
|
||||
button.addEventListener('click', function() {
|
||||
logs.push('click2');
|
||||
});
|
||||
let listeners = button.eventListeners!('click');
|
||||
expect(listeners.length).toBe(2);
|
||||
|
||||
button.dispatchEvent(clickEvent);
|
||||
expect(logs.length).toBe(2);
|
||||
expect(logs).toEqual(['click1', 'click2']);
|
||||
|
||||
setTimeout(() => {
|
||||
logs = [];
|
||||
button.dispatchEvent(clickEvent);
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs).toEqual(['click2']);
|
||||
done();
|
||||
}, 10);
|
||||
}));
|
||||
|
||||
it('should support reschedule eventTask',
|
||||
ifEnvSupports(supportEventListenerOptions, function() {
|
||||
let hookSpy1 = jasmine.createSpy('spy1');
|
||||
|
||||
Reference in New Issue
Block a user