Skip to content

Commit a7ec227

Browse files
JiaLiPassionamilamen
authored andcommitted
fix(zone.js): support addEventListener with signal option. (angular#49595)
Close angular#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 angular#49595
1 parent ad7ec6b commit a7ec227

File tree

3 files changed

+146
-18
lines changed

3 files changed

+146
-18
lines changed

packages/zone.js/lib/common/events.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,16 @@ export function patchEventTarget(
293293
// if task is not marked as isRemoved, this call is directly
294294
// from Zone.prototype.cancelTask, we should remove the task
295295
// from tasksList of target first
296+
const signal = task?.options?.signal;
297+
if (typeof signal === 'object' && signal?.tasks) {
298+
const abortTasks = signal.tasks;
299+
for (let i = 0; i < abortTasks?.length || 0; i++) {
300+
if (abortTasks[i] === task) {
301+
abortTasks.splice(i, 1);
302+
break;
303+
}
304+
}
305+
}
296306
if (!task.isRemoved) {
297307
const symbolEventNames = zoneSymbolEventNames[task.eventName];
298308
let symbolEventName;
@@ -394,6 +404,11 @@ export function patchEventTarget(
394404
const passive =
395405
passiveSupported && !!passiveEvents && passiveEvents.indexOf(eventName) !== -1;
396406
const options = buildEventListenerOptions(arguments[2], passive);
407+
const signal = typeof options === 'object' && options?.signal;
408+
if (typeof signal === 'object' && signal?.aborted) {
409+
// the signal is an aborted one, just return without attaching the event listener.
410+
return;
411+
}
397412

398413
if (unpatchedEvents) {
399414
// check unpatched list
@@ -465,9 +480,31 @@ export function patchEventTarget(
465480
(data as any).taskData = taskData;
466481
}
467482

483+
if (signal && typeof signal === 'object') {
484+
// if addEventListener with signal options, we don't pass it to
485+
// native addEventListener, instead we keep the signal setting
486+
// and handle ourselves.
487+
taskData.options.signal = undefined;
488+
}
468489
const task: any =
469490
zone.scheduleEventTask(source, delegate, data, customScheduleFn, customCancelFn);
470491

492+
if (signal && typeof signal === 'object') {
493+
// after task is scheduled, we need to store the signal back to task.options
494+
taskData.options.signal = signal;
495+
const tasks = signal.tasks || [];
496+
tasks.push(task);
497+
signal.tasks = tasks;
498+
if (!signal[Zone.__symbol__('abortListener')]) {
499+
signal[Zone.__symbol__('abortListener')] = true;
500+
nativeListener.call(signal, 'abort', function() {
501+
const sTasks = signal.tasks.slice();
502+
sTasks.forEach((task: Task) => task.zone.cancelTask(task));
503+
signal.tasks.length = 0;
504+
});
505+
}
506+
}
507+
471508
// should clear taskData.target to avoid memory leak
472509
// issue, https://github.com/angular/angular/issues/20442
473510
taskData.target = null;

packages/zone.js/lib/common/fetch.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,7 @@ Zone.__load_patch('fetch', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
2929
const fetchTaskAborting = api.symbol('fetchTaskAborting');
3030
const OriginalAbortController = global['AbortController'];
3131
const supportAbort = typeof OriginalAbortController === 'function';
32-
let abortNative: Function|null = null;
33-
if (supportAbort) {
34-
global['AbortController'] = function() {
35-
const abortController = new OriginalAbortController();
36-
const signal = abortController.signal;
37-
signal.abortController = abortController;
38-
return abortController;
39-
};
40-
abortNative = api.patchMethod(
41-
OriginalAbortController.prototype, 'abort',
42-
(delegate: Function) => (self: any, args: any) => {
43-
if (self.task) {
44-
return self.task.zone.cancelTask(self.task);
45-
}
46-
return delegate.apply(self, args);
47-
});
48-
}
32+
let abortNative: Function|null = OriginalAbortController?.prototype[api.symbol('abort')];
4933
const placeholder = function() {};
5034
global['fetch'] = function() {
5135
const args = Array.prototype.slice.call(arguments);
@@ -105,7 +89,7 @@ Zone.__load_patch('fetch', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
10589
}
10690
});
10791
if (signal && signal.abortController) {
108-
signal.abortController.task = task;
92+
signal.abortController.tasks = [task];
10993
}
11094
});
11195
};

packages/zone.js/test/browser/browser.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,6 +1762,113 @@ describe('Zone', function() {
17621762
expect(logs).toEqual(['click2']);
17631763
});
17641764

1765+
it('should support remove event listeners via AbortController', function() {
1766+
let logs: string[] = [];
1767+
const ac = new AbortController();
1768+
1769+
button.addEventListener('click', function() {
1770+
logs.push('click1');
1771+
}, {signal: ac.signal});
1772+
button.addEventListener('click', function() {
1773+
logs.push('click2');
1774+
});
1775+
button.addEventListener('click', function() {
1776+
logs.push('click3');
1777+
}, {signal: ac.signal});
1778+
let listeners = button.eventListeners!('click');
1779+
expect(listeners.length).toBe(3);
1780+
1781+
button.dispatchEvent(clickEvent);
1782+
expect(logs.length).toBe(3);
1783+
expect(logs).toEqual(['click1', 'click2', 'click3']);
1784+
ac.abort();
1785+
logs = [];
1786+
1787+
listeners = button.eventListeners!('click');
1788+
button.dispatchEvent(clickEvent);
1789+
expect(logs.length).toBe(1);
1790+
expect(listeners.length).toBe(1);
1791+
expect(logs).toEqual(['click2']);
1792+
});
1793+
1794+
it('should support remove event listeners with AbortController', function() {
1795+
let logs: string[] = [];
1796+
const ac = new AbortController();
1797+
1798+
const listener1 = function() {
1799+
logs.push('click1');
1800+
};
1801+
button.addEventListener('click', listener1, {signal: ac.signal});
1802+
button.addEventListener('click', function() {
1803+
logs.push('click2');
1804+
});
1805+
let listeners = button.eventListeners!('click');
1806+
expect(listeners.length).toBe(2);
1807+
1808+
button.dispatchEvent(clickEvent);
1809+
expect(logs.length).toBe(2);
1810+
expect(logs).toEqual(['click1', 'click2']);
1811+
1812+
button.removeEventListener('click', listener1);
1813+
listeners = button.eventListeners!('click');
1814+
expect(listeners.length).toBe(1);
1815+
1816+
logs = [];
1817+
1818+
listeners = button.eventListeners!('click');
1819+
button.dispatchEvent(clickEvent);
1820+
expect(logs.length).toBe(1);
1821+
expect(listeners.length).toBe(1);
1822+
expect(logs).toEqual(['click2']);
1823+
1824+
ac.abort();
1825+
expect(logs).toEqual(['click2']);
1826+
});
1827+
1828+
it('should not add event listeners with aborted signal', function() {
1829+
let logs: string[] = [];
1830+
1831+
button.addEventListener('click', function() {
1832+
logs.push('click1');
1833+
}, {signal: AbortSignal.abort()});
1834+
button.addEventListener('click', function() {
1835+
logs.push('click2');
1836+
});
1837+
let listeners = button.eventListeners!('click');
1838+
expect(listeners.length).toBe(1);
1839+
1840+
button.dispatchEvent(clickEvent);
1841+
expect(logs.length).toBe(1);
1842+
expect(logs).toEqual(['click2']);
1843+
});
1844+
1845+
it('should remove event listeners with timeout signal',
1846+
ifEnvSupportsWithDone(
1847+
() => typeof AbortSignal.timeout === 'function', function(done: DoneFn) {
1848+
let logs: string[] = [];
1849+
1850+
button.addEventListener('click', function() {
1851+
logs.push('click1');
1852+
}, {signal: AbortSignal.timeout(1)});
1853+
button.addEventListener('click', function() {
1854+
logs.push('click2');
1855+
});
1856+
let listeners = button.eventListeners!('click');
1857+
expect(listeners.length).toBe(2);
1858+
1859+
button.dispatchEvent(clickEvent);
1860+
expect(logs.length).toBe(2);
1861+
expect(logs).toEqual(['click1', 'click2']);
1862+
1863+
setTimeout(() => {
1864+
logs = [];
1865+
button.dispatchEvent(clickEvent);
1866+
expect(logs.length).toBe(1);
1867+
expect(logs).toEqual(['click2']);
1868+
done();
1869+
}, 10);
1870+
}));
1871+
17651872
it('should support reschedule eventTask',
17661873
ifEnvSupports(supportEventListenerOptions, function() {
17671874
let hookSpy1 = jasmine.createSpy('spy1');

0 commit comments

Comments
 (0)