6 Ways to Unsubscribe from Observables in Angular

栏目: IT技术 · 发布时间: 4年前

内容简介:In this post, I’ll review the different ways you can unsubscribe from Observables in Angular apps.Observables have theNow, if we subscribe to a stream the stream will be left open and the callback will be called when values are emitted into it anywhere in

A review of the different ways you can unsubscribe from Observables in Angular

6 Ways to Unsubscribe from Observables in Angular

In this post, I’ll review the different ways you can unsubscribe from Observables in Angular apps.

The Downside to Observable Subscription

Observables have the subscribe method we call with a callback function to get the values emitted into the Observable. In Angular, we use it in Components/Directives especially in the router module, NgRx, HTTP module.

Now, if we subscribe to a stream the stream will be left open and the callback will be called when values are emitted into it anywhere in the app until they are closed by calling the unsubscribe method.

@Component({...})
export class AppComponent implements OnInit {
    subscription
    ngOnInit () {
        var observable = Rx.Observable.interval(1000);
        this.subscription = observable.subscribe(x => console.log(x));
    }
}

Looking at the above implementation, we called the interval to emit values at the interval of 1sec. We subscribe to it to receive the emitted value, our function callback will log the emitted value on the browser console.

Now, if this AppComponent is destroyed, maybe via navigating away from the component or using the destroy(...) method, we will still be seeing the console log on the browser. This is because the AppComponent has been destroyed but the subscription still lives on, it hasn't been canceled.

If a subscription is not closed the function callback attached to it will be continuously called, this poses a huge memory leak and performance issue.

If the function callback in our AppCompoennt subscription had been an expensive function, we will see that the function will still be called despite its parent being destroyed this will eat up resources and slow down the overall app performance.

Tip:Use Bit ( Github ) to “harvest” Angular components from any codebase and share them on bit.dev . Let your team reuse and collaborate on components to write scalable code, speed up development and maintain a consistent UI.

6 Ways to Unsubscribe from Observables in Angular
Example: searching for shared components on bit.dev

1. Use the unsubscribe method

A Subscription essentially just has an unsubscribe() function to release resources or cancel Observable executions.

To prevent this memory leaks we have to unsubscribe from the subscriptions when we are done with. We do so by calling the unsubscribe method in the Observable.

In Angular, we have to unsubscribe from the Observable when the component is being destroyed. Luckily, Angular has a ngOnDestroy hook that is called before a component is destroyed, this enables devs to provide the cleanup crew here to avoid hanging subscriptions, open portals, and what nots that may come in the future to bite us in the back.

So, whenever we use Observables in a component in Angular, we should set up the ngOnDestroy method, and call the unsubscribe method on all of them.

@Component({...})
export class AppComponent implements OnInit, OnDestroy {
    subscription
    ngOnInit () {
        var observable = Rx.Observable.interval(1000);
        this.subscription = observable.subscribe(x => console.log(x));
    }    ngOnDestroy() {
        this.subscription.unsubscribe()
    }
}

We added ngOnDestroy to our AppCompoennt and called unsubscribe method on the this.subscription Observable. When the AppComponent is destroyed (via route navigation, destroy(...), etc) there will be no hanging subscription, the interval will be canceled and there will be no console logs in the browser anymore.

If there are multiple subscriptions:

@Component({...})
export class AppComponent implements OnInit, OnDestroy {
    subscription1$
    subscription2$    ngOnInit () {
        var observable1$ = Rx.Observable.interval(1000);
        var observable2$ = Rx.Observable.interval(400);
        this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));
        this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x));
    }    ngOnDestroy() {
        this.subscription1$.unsubscribe()
        this.subscription2$.unsubscribe()
    }
}

There are two subscriptions in AppComponent. They are both unsubscribed in the ngOnDestroy hook preventing memory leaks.

We can gather them subscriptions in an array and unsubscribe from them in the ngOnDestroy :

@Component({...})
export class AppComponent implements OnInit, OnDestroy {
    subscription1$
    subscription2$
    subscriptions: Subscription = []    ngOnInit () {
        var observable1$ = Rx.Observable.interval(1000);
        var observable2$ = Rx.Observable.interval(400);
        this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));
        this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x));
        this.subscriptions.push(this.subscription1$)
        this.subscriptions.push(this.subscription2$)
    }    ngOnDestroy() {
        this.subscriptions.forEach((subscription) => subscription.unsubscribe())
    }
}

Observables subscribe method returns an object of RxJS’s Subscription type. This Subscription represents a disposable resource. These Subscriptions can be grouped using the add method, this will attach a child Subscription to the current Subscription. When a Subscription is unsubscribed, all its children will be unsubscribed as well. We can refactor our AppComponent to use this:

@Component({...})
export class AppComponent implements OnInit, OnDestroy {
    subscription1$
    subscription2$
    subscription: Subscription    ngOnInit () {
        var observable1$ = Rx.Observable.interval(1000);
        var observable2$ = Rx.Observable.interval(400);
        this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));
        this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x));
        this.subscription.add(this.subscription1$)
        this.subscription.add(this.subscription2$)
    }    ngOnDestroy() {
        this.subscription.unsubscribe()
    }
}

This will unsubscribe this.subscripton1$ , and this.subscripton2$ when the component is destroyed.

2. Use Async | Pipe

The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.

Using it in our AppComponent:

@Component({
    ...,
    template: `
        <div>
         Interval: {{observable$ | async}}
        </div>
    `
})
export class AppComponent implements OnInit, OnDestroy {
    observable$
    ngOnInit () {
        this.observable$ = Rx.Observable.interval(1000);
    }
}

On instantiation, the AppComponent will create an Observable from the interval method. In the template, the Observable observable$ is piped to the async Pipe. The async pipe will subscribe to the observable$ and display its value in the DOM. async pipe will unsubscribe the observable$ when the AppComponent is destroyed. async Pipe has ngOnDestroy on its class so it is called when the view is contained in is being destroyed.

Using the async pipe is a huge advantage if we are using Observables in our components because it will subscribe to them and unsubscribe from them. We will not be bothered about forgetting to unsubscribe from them in ngOnDestroy when the component is being killed off.

3. Use RxJS take* operators

RxJS have useful operators that we can use in a declarative way to unsubscribe from subscriptions in our Angular project. One of them are the take* family operators:

  • take(n)
  • takeUntil(notifier)
  • takeWhile(predicate)

take(n)

This operator makes a subscription happen once. This operator makes a source subscription happen the number of n times specified and completes.

1 is popularly used with the take operator so subscriptions happen once and exit.

This operator will be effective when we want a source Observable to emit once and then unsubscribe from the stream:

@Component({
    ...
})
export class AppComponent implements OnInit {
    observable$
    ngOnInit () {
        this.observable$ = Rx.Observable.interval(1000);
        this.observable$.pipe(take(1)).
        subscribe(x => console.log(x))
    }
}

The observable$ Observable will unsubscribe when the interval emits the first value.

Beware, even if the AppComponent is destroyed the observable$ will not unsubscribe until the interval emits a value.

So it is best to make sure everything is canceled in the ngOnDestroy hook:

@Component({
    ...
})
export class AppComponent implements OnInit, OnDestroy {
    observable$
    ngOnInit () {
        this.observable$ = Rx.Observable.interval(1000);
        this.observable$.pipe(take(1)).
        subscribe(x => console.log(x))
    }    ngOnDestroy() {
        this.observable$.unsubscribe()
    }
}

or to make sure that the source Observable is fired during the component lifetime.

takeUntil(notifier)

This operator emits values emitted by the source Observable until a notifier Observable emits a value.

@Component({...})
export class AppComponent implements OnInit {
    notifier = new Subject()    ngOnInit () {
        var observable$ = Rx.Observable.interval(1000);
        observable$.pipe(takeUntil(this.notifier))
        .subscribe(x => console.log(x));
    }    ngOnDestroy() {
        this.notifier.next()
        this.notifier.complete()
    }
}

We have an extra notifier Subject, this is what will emit to make the this.subscription unsubscribe. See, we pipe the observable to takeUntil before we subscribe. The takeUntil will emit the values emitted by the interval until the notifier Subject emits, it will then unsubscribe the observable$. The best place to make the notifier to emit so the observable$ is canceled is in the ngOnDestroy hook.

takeWhile(predicate)

This operator will emit the value emitted by the source Observable so long as the emitted value passes the test condition of the predicate.

@Component({...})
export class AppComponent implements OnInit {
    ngOnInit () {
        var observable$ = Rx.Observable.interval(1000);
        observable$.pipe(takeWhile(value => value < 10))
        .subscribe(x => console.log(x));
    }
}

We pipe the observable$ to go through takeWhile operator. The takeWhile operator will pass the values so long as the values as less than 10. If it encounters a value greater than or equal to 10 the operator will unsubscribe the observable$.

If the interval doesn’t emit up to 9 values before the AppComponent is destroyed, the observable$ subscription will still be open until the interval emits 10 before it is destroyed. So for safety, we add the ngOnDestroy hook to unsubscribe observable$ when the component is destroyed.

@Component({...})
export class AppComponent implements OnInit, OnDestroy {
    observable$
    ngOnInit () {
        this.observable$ = Rx.Observable.interval(1000);
        this.observable$.pipe(takeWhile(value => value < 10))
        .subscribe(x => console.log(x));
    }    ngOnDestroy() {
        this.observable$.unsubscribe()
    }
}

4. Use RxJS first operator

This operator is like the concatenation of take(1) and takeWhile :grin:

If called with no value, it emits the first value emitted by the source Observable and completes. If it is called with a predicate function, it emits the first value of the source Observable that pass the test condition of the predicate function and complete.

@Component({...})
export class AppComponent implements OnInit {
    observable$
    ngOnInit () {
        this.observable = Rx.Observable.interval(1000);
        this.observable$.pipe(first())
        .subscribe(x => console.log(x));
    }
}

The observable$ will complete if the interval emits its first value. That means, in our console, we will see only 1 log and nothing else. At point will stop emitting values.

@Component({...})
export class AppComponent implements OnInit {
    observable$
    ngOnInit () {
        this.observable$ = Rx.Observable.interval(1000);
        this.observable$.pipe(first(val => val === 10))
        .subscribe(x => console.log(x));
    }
}

Here, first will not emit a value until interval emits a value that is equal to 10, then it will complete the observable$. In the console will see only one log and no more.

In the first example, if the AppComponent is destroyed before first receives a value from observable$, the subscription will still be open until the interval emits its first value.

Also, in the second example, if the AppComponent is destroyed before interval produces a value that passes the first operator condition, the subscription will still be open up until interval emits 10.

So to ensure safety, we have to explicitly cancel the subscriptions in the ngOnDestroy hook when the component is destroyed.

5. Use Decorator to automate Unsubscription

We are humans, we can forget, it is our nature. Most methods we have seen here relies on the ngOnDestroy hook to make sure we clean up the subscriptions in the component when destroyed. We can forget to clean them up in the ngOnDestroy, maybe due to deadline leaning dangerously near and we have a grumpy client or a psycho client that knows where you live oh boy

We can leverage the usefulness of Decorator in our Angular projects to help us add the ngOnDestroy method in our components and unsubscribe from all the subscriptions in our component automatically.

Here is an implementation that will be of huge help:

function AutoUnsub() {
    return function(constructor) {
        const orig = constructor.prototype.ngOnDestroy
        constructor.prototype.ngOnDestroy = function() {
            for(const prop in this) {
                const property = this[prop]
                if(typeof property.subscribe === "function") {
                    property.unsubscribe()
                }
            }
            orig.apply()
        }
    }
}

This AutoUnsub is a class decorator that can be applied to classes in our Angular project. See, it saves the original ngOnDestroy hook, then creates a new one and hook it into the class it is applied on. So, when the class is being destroyed the new hook is called. Inside it, the functions scan through the properties of the class, if it finds an Observable property it will unsubscribe from it. Then it calls the original ngOnDestroy hook in the class if present.

@Component({
    ...
})
@AutoUnsub
export class AppComponent implements OnInit {
    observable$
    ngOnInit () {
        this.observable$ = Rx.Observable.interval(1000);
        this.observable$.subscribe(x => console.log(x))
    }
}

We apply it on our AppComponent class no longer bothered about unsubscribing the observable$ in the ngOnDestroy, the Decorator will do it for us.

There are downsides to this(what isn’t in this world), it will be a problem if we have non-subscribing Observable in our component.

6. Use tslint

Some might need a reminder by tslint , to remind us in the console that our components or directives should have a ngOnDestroy method when it detects none.

We can add a custom rule to tslint to warn us in the console during linting and building if it finds no ngOnDestroy hook in our components:

// ngOnDestroyRule.tsimport * as Lint from "tslint"
import * as ts from "typescript"
import * as tsutils from "tsutils"export class Rule extends Lint.Rules.AbstractRule {
    public static metadata: Lint.IRuleMetadata = {
        ruleName: "ng-on-destroy",
        description: "Enforces ngOnDestory hook on component/directive/pipe classes",
        optionsDescription: "Not configurable.",
        options: null,
        type: "style",
        typescriptOnly: false
    }    public static FAILURE_STRING = "Class name must have the ngOnDestroy hook";    public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
        return this.applyWithWalker(new NgOnDestroyWalker(sourceFile, Rule.metadata.ruleName, void this.getOptions()))
    }
}class NgOnDestroyWalker extends Lint.AbstractWalker {    visitClassDeclaration(node: ts.ClassDeclaration) {
        this.validateMethods(node)
    }    validateMethods(node: ts.ClassDeclaration) {
        const methodNames = node.members.filter(ts.isMethodDeclaration).map(m => m.name!.getText());
        const ngOnDestroyArr = methodNames.filter( methodName => methodName === "ngOnDestroy")
        if( ngOnDestroyArr.length === 0)
            this.addFailureAtNode(node.name, Rule.FAILURE_STRING);
    }
}

If we have a component like this without ngOnDestroy:

@Component({
    ...
})
export class AppComponent implements OnInit {
    observable$
    ngOnInit () {
        this.observable$ = Rx.Observable.interval(1000);
        this.observable$.subscribe(x => console.log(x))
    }
}

Linting AppComponent will warn us about the missing ngOnDestroy hook:

$ ng lint
Error at app.component.ts 12: Class name must have the ngOnDestroy hook

:100:Helpful Links:100:

Conclusion

We have seen ways that we can unsubscribe from our Angular projects. Like we have learned, hanging or open subscriptions may introduce memory leaks, bugs, unwanted behavior or performance cost to our Angular apps. Find the best possible ways for you here and plug those leaks today.

Social Media links


以上所述就是小编给大家介绍的《6 Ways to Unsubscribe from Observables in Angular》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

这才是马云

这才是马云

陈伟 / 浙江人民出版社 / 2011-5 / 30.00元

“幽默马云”、“开心马云”、“顽皮马云”、“狂妄马云”等。《这才是马云》从各个角度揭开了千面马云的真面目,告诉你一个与想象中大不一样的马云。这不只是一本书,更像一部喜剧电影,让你通过声音、色彩、表情等诸多要素走近马云,感受阿里巴巴。没有冗长的说教,只有让人忍俊不禁的细节;没有高深的理论,只有通俗、诚恳的陈述。作者借幽默平常的琐事,记录下马云“可爱”的一面,看完后让人恍然大悟:原来,马云是这样一个人......一起来看看 《这才是马云》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具