Hello! Let's continue building our reactive library.
In our last lesson, we defined the Observer interface, establishing the "contract" for how a consumer receives values, errors, and completion signals from a data stream. We now have the "what" for data consumption. Today, we'll focus on managing the connection itself.
This lesson addresses the learning outcome: Implement a Subscription class with teardown logic for resource management. A Subscription acts as a handle to an active observation. Its most critical role is to provide a way to unsubscribe, allowing us to clean up resources and prevent common issues like memory leaks. This is a fundamental concept for building robust asynchronous applications.
1. The "Why": Preventing Memory Leaks
In front-end development, you've likely encountered situations where you add an event listener but forget to remove it. If the element is removed from the DOM, the listener can keep a reference to it, preventing garbage collection. This is a classic memory leak.
Observables that represent long-lived or infinite event sources (like DOM events, WebSockets, or timers) present the same risk. If we subscribe to an observable but never signal that we're no longer interested, the subscription might live on in memory indefinitely.
To see a practical demonstration of this problem and its solution, let's watch a segment from the Academind tutorial we looked at previously.
OBSERVABLES, OBSERVERS & SUBSCRIPTIONS | RxJS TUTORIAL
This part of the "OBSERVABLES, OBSERVERS & SUBSCRIPTIONS" video clearly explains why unsubscribing is crucial for infinite observables and demonstrates how calling unsubscribe() stops the flow of data.
About 15 minutes into the video, watch memory leak fix. Focus on the concept of a memory leak in the context of an infinite observable and see how the unsubscribe method on the subscription object solves this.
This illustrates the core purpose of a Subscription: it represents an active execution and gives us the power to terminate it.
2. The "What": Subscription and Teardown Logic
Now that we understand the "why," let's formalize the "what." What exactly is a Subscription, and how does it manage cleanup?
A Subscription is an object that represents a disposable resource, usually the execution of an Observable. It has one primary method, unsubscribe(), which is called to release resources and cancel the execution.
When unsubscribe() is called, it needs to execute some cleanup code. This is often called teardown logic. This logic is responsible for undoing whatever setup was done when the subscription started. For example:
- If the observable wrapped
setInterval, the teardown logic would beclearInterval. - If it wrapped
addEventListener, the teardown logic would beremoveEventListener.
The following article provides an excellent, in-depth explanation of the Subscription lifecycle and the role of teardown logic.
To unsubscribe, or not to unsubscribe, that is the question.
The article "To unsubscribe, or not to unsubscribe, that is the question" by Jurek Wozniak is a great resource. We'll read the sections that define a Subscription and explain what happens when it ends.
Please read the following three sections. In the 'What is a Subscription?' section, which comes after the opening discussion on what happens if you don't unsubscribe, read the Subscription definition. Next, in 'How can a Subscription end?', read ways Subscriptions end. Finally, in 'What happens after the Subscription ends?', read teardown logic. Focus on the definition of a Subscription, the three ways it can end (complete, error, unsubscribe), and the concept of 'Finalizer/Teardown logic'.
The key takeaway is that a subscription can end in three ways, but regardless of how it ends (naturally via complete/error or manually via unsubscribe), the same teardown logic should be executed to ensure resources are always released. Our Subscription class will be the mechanism that guarantees this.
3. The "How": Implementing the Subscription Class
We're now ready to implement the Subscription class. It needs to do two main things:
- Store the teardown logic that it's responsible for.
- Provide an
unsubscribe()method that executes this logic and ensures it only runs once.
Let's start by creating the file and the basic class structure.
Your Task: Create and Implement the Subscription Class
- In your project's
srcdirectory, create a new file namedSubscription.ts. - Add the following code to the file.
// src/Subscription.ts
/**
* Represents the execution of an Observable. A Subscription has one important
* method, `unsubscribe`, that takes no argument and just disposes of the
* resource held by the subscription.
*/
export class Subscription {
/**
* A flag to indicate whether this Subscription has already been unsubscribed.
* @private
*/
private _closed = false;
/**
* The teardown logic to be executed when the subscription is unsubscribed.
* This is a function that will be called to clean up resources.
* @private
*/
private _teardown: (() => void) | null;
/**
* @param teardown A function to call when the subscription is unsubscribed.
*/
constructor(teardown?: () => void) {
this._teardown = teardown || null;
}
/**
* A getter to check if the subscription is closed.
* @returns `true` if the subscription is closed, `false` otherwise.
*/
get closed(): boolean {
return this._closed;
}
/**
* Disposes the resources held by the subscription by executing the
* teardown logic. If the subscription is already closed, this method
* does nothing.
*/
unsubscribe(): void {
if (this._closed) {
return; // Do nothing if already unsubscribed
}
this._closed = true;
// Execute the teardown logic if it exists
if (this._teardown) {
this._teardown();
}
}
}
Let's break down this implementation:
_closed: A private boolean flag that prevents the teardown logic from being executed multiple times. Theunsubscribe()method is idempotent._teardown: A private property to hold the cleanup function. We make it optional in the constructor, as some observables might not need any cleanup (e.g., an observable that synchronously emits a few values and completes).constructor(teardown?: () => void): It accepts an optionalteardownfunction and stores it.closedgetter: A public way to check the status of the subscription without allowing external code to modify it.unsubscribe(): This is the public API. It checks the_closedflag, and if the subscription is active, it sets the flag totrueand then executes the stored_teardownfunction.
4. Connecting Subscription to the Observable
We've built the Subscription class, but where does the teardown function passed to its constructor come from?
This is the responsibility of the producer function inside the Observable. When we implement our Observable class in the next lesson, its constructor will accept a function (the producer). This producer function receives the Observer as an argument and is responsible for generating values. Crucially, this producer can return a function, and that returned function is our teardown logic.
The Observable.subscribe() method will then:
- Execute the producer function.
- Capture the returned teardown logic.
- Create a new
Subscriptioninstance, passing it that teardown logic. - Return the new
Subscriptionto the caller.
Let's look at a conceptual example of this flow.
Demystifying RxJS, Part I: Building our own Observables
The article "Demystifying RxJS, Part I" provides a clear explanation of how the teardown function is returned from the observable's core logic.
In the 'Observables' section, read the two paragraphs that start with 'If there is any cleanup logic...': teardown functions. Then, in the subsection 'Solidifying our understanding: Observable factories', look at the code example for fromEvent(), which is introduced in the paragraph beginning "Finally, let's implement fromEvent()": the fromEvent implementation. Notice how it returns a function that calls removeEventListener. This is a perfect example of teardown logic.
This pattern creates a clean separation of concerns:
- The producer knows how to set up and tear down its specific resources.
- The
Subscriptionclass knows how to manage the lifecycle of the teardown. - The consumer of the observable simply calls
unsubscribe()without needing to know any of the implementation details.
Conclusion
In this lesson, we have built a crucial piece of our reactive library's infrastructure. The Subscription class provides the resource management capabilities necessary for a robust and leak-free system.
Key Takeaways:
- A
Subscriptionrepresents an active, ongoing execution of an Observable. - Calling the
unsubscribe()method is the primary way to manually terminate a subscription and clean up resources. - Teardown logic is the cleanup code (e.g.,
clearInterval,removeEventListener) that is executed upon unsubscription. - Our
Subscriptionclass encapsulates this behavior by storing a teardown function and executing it once whenunsubscribe()is called. - The teardown logic itself is provided by the Observable's internal producer function, creating a clean contract.
Preview of the Next Lesson:
We now have the Observer (the what) and the Subscription (the how). In our next lesson, we will finally bring these two pieces together by implementing the central Observable class. We will implement its subscribe method, which will connect an Observer to a producer function and return the Subscription that manages the lifecycle of that connection.
Can't find a good explanation? Sign up and we'll make it for you
Sign up