Skip to content

Commit fb375a0

Browse files
authored
perf: removed code that would bind functions passed with observers to subscribe. (#6815)
* refactor(SafeSubscriber): optimize perf for ordinary observers No longer require function binding if we aren't using the deprecated next context. This should improve performance in the common path of consumers subscribing with an object or even with a function. Adds a simple class `ConsumerObserver` which is mostly meant to optimize the number of function refrences created. We should never expose this externally. Related #6783 * chore: update comments * refactor(Subscriber): reduce property access
1 parent 481313d commit fb375a0

1 file changed

Lines changed: 71 additions & 42 deletions

File tree

src/internal/Subscriber.ts

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,49 @@ function bind<Fn extends (...args: any[]) => any>(fn: Fn, thisArg: any): Fn {
147147
return _bind.call(fn, thisArg);
148148
}
149149

150+
/**
151+
* Internal optimization only, DO NOT EXPOSE.
152+
* @internal
153+
*/
154+
class ConsumerObserver<T> implements Observer<T> {
155+
constructor(private partialObserver: Partial<Observer<T>>) {}
156+
157+
next(value: T): void {
158+
const { partialObserver } = this;
159+
if (partialObserver.next) {
160+
try {
161+
partialObserver.next(value);
162+
} catch (error) {
163+
handleUnhandledError(error);
164+
}
165+
}
166+
}
167+
168+
error(err: any): void {
169+
const { partialObserver } = this;
170+
if (partialObserver.error) {
171+
try {
172+
partialObserver.error(err);
173+
} catch (error) {
174+
handleUnhandledError(error);
175+
}
176+
} else {
177+
handleUnhandledError(err);
178+
}
179+
}
180+
181+
complete(): void {
182+
const { partialObserver } = this;
183+
if (partialObserver.complete) {
184+
try {
185+
partialObserver.complete();
186+
} catch (error) {
187+
handleUnhandledError(error);
188+
}
189+
}
190+
}
191+
}
192+
150193
export class SafeSubscriber<T> extends Subscriber<T> {
151194
constructor(
152195
observerOrNext?: Partial<Observer<T>> | ((value: T) => void) | null,
@@ -155,65 +198,51 @@ export class SafeSubscriber<T> extends Subscriber<T> {
155198
) {
156199
super();
157200

158-
let next: ((value: T) => void) | undefined;
159-
if (isFunction(observerOrNext)) {
201+
let partialObserver: Partial<Observer<T>>;
202+
if (isFunction(observerOrNext) || !observerOrNext) {
160203
// The first argument is a function, not an observer. The next
161204
// two arguments *could* be observers, or they could be empty.
162-
next = observerOrNext;
163-
} else if (observerOrNext) {
164-
// The first argument is an observer object, we have to pull the handlers
165-
// off and capture the owner object as the context. That is because we're
166-
// going to put them all in a new destination with ensured methods
167-
// for `next`, `error`, and `complete`. That's part of what makes this
168-
// the "Safe" Subscriber.
169-
({ next, error, complete } = observerOrNext);
205+
partialObserver = {
206+
next: observerOrNext ?? undefined,
207+
error: error ?? undefined,
208+
complete: complete ?? undefined,
209+
};
210+
} else {
211+
// The first argument is a partial observer.
170212
let context: any;
171213
if (this && config.useDeprecatedNextContext) {
172214
// This is a deprecated path that made `this.unsubscribe()` available in
173215
// next handler functions passed to subscribe. This only exists behind a flag
174216
// now, as it is *very* slow.
175217
context = Object.create(observerOrNext);
176218
context.unsubscribe = () => this.unsubscribe();
219+
partialObserver = {
220+
next: observerOrNext.next && bind(observerOrNext.next, context),
221+
error: observerOrNext.error && bind(observerOrNext.error, context),
222+
complete: observerOrNext.complete && bind(observerOrNext.complete, context),
223+
};
177224
} else {
178-
context = observerOrNext;
225+
// The "normal" path. Just use the partial observer directly.
226+
partialObserver = observerOrNext;
179227
}
180-
next = next && bind(next, context);
181-
error = error && bind(error, context);
182-
complete = complete && bind(complete, context);
183228
}
184229

185-
// Once we set the destination, the superclass `Subscriber` will
186-
// do it's magic in the `_next`, `_error`, and `_complete` methods.
187-
this.destination = {
188-
next: next ? wrapForErrorHandling(next, this) : noop,
189-
error: wrapForErrorHandling(error ?? defaultErrorHandler, this),
190-
complete: complete ? wrapForErrorHandling(complete, this) : noop,
191-
};
230+
// Wrap the partial observer to ensure it's a full observer, and
231+
// make sure proper error handling is accounted for.
232+
this.destination = new ConsumerObserver(partialObserver);
192233
}
193234
}
194235

195-
/**
196-
* Wraps a user-provided handler (or our {@link defaultErrorHandler} in one case) to
197-
* ensure that any thrown errors are caught and handled appropriately.
198-
*
199-
* @param handler The handler to wrap
200-
* @param instance The SafeSubscriber instance we're going to mark if there's an error.
201-
*/
202-
function wrapForErrorHandling(handler: (arg?: any) => void, instance: SafeSubscriber<any>) {
203-
return (...args: any[]) => {
204-
try {
205-
handler(...args);
206-
} catch (err) {
207-
if (config.useDeprecatedSynchronousErrorHandling) {
208-
captureError(err);
209-
} else {
210-
// Ideal path, we report this as an unhandled error,
211-
// which is thrown on a new call stack.
212-
reportUnhandledError(err);
213-
}
214-
}
215-
};
236+
function handleUnhandledError(error: any) {
237+
if (config.useDeprecatedSynchronousErrorHandling) {
238+
captureError(error);
239+
} else {
240+
// Ideal path, we report this as an unhandled error,
241+
// which is thrown on a new call stack.
242+
reportUnhandledError(error);
243+
}
216244
}
245+
217246
/**
218247
* An error handler used when no error handler was supplied
219248
* to the SafeSubscriber -- meaning no error handler was supplied

0 commit comments

Comments
 (0)