DEV Community

Cover image for Callbacks and References: Allowing the Garbage to get Collected
𒎏Wii 🏳️‍⚧️
𒎏Wii 🏳️‍⚧️

Posted on • Updated on

Callbacks and References: Allowing the Garbage to get Collected

JavaScript being a garbage-collected language, we don't usually have to concern ourselves with questions of allocating and releasing objects. But occasionally, specially when dealing with callbacks, it is easy to keep objects alive indefinitely, even if there are no longer any meaningful references to this.

There's several tools the language provides us to deal with these situations:

  • WeakRef to store a single weak reference to an object
  • WeakMap to associate values with objects only as long as they exist
  • WeakSet to remember objects as long as they exist
  • FinalizationRegistry to do something when an object gets collected

Depending on the situation, one may need one or another of these features, but the case I want to describe today will make use of the first and the last.

A common case is for objects to care about certain external state changes for as long as they exist. For example, a custom element might want to listen for "scroll" events on the window object. But naively adding an event listener to window means to keep a reference to the object. If these custom elements are short-lived but many in numbers, then they will accumulate in memory, and the additional event listeners will also pile up and waste processing power.

Here's a simple example of something like this:

class MyElement extends HTMLElement {
   constructor() {
      super()
      window.addEventListener("scroll", event => {
         this.handleScroll()
      })
   }

   handleScroll() {
      this.classList.toggle("top", window.scrollY == 0)
   }
}
Enter fullscreen mode Exit fullscreen mode

What we want is to remove the event listener by the time the object gets garbage-collected. To achieve this, we can make use of two features:

Firstly, replacing the strong reference to this in the event listener with a WeakRef will prevent the event listener from keeping the object alive if no other references to it exist. Once the object has been collected, the deref() method will just return undefined.

const ref = new WeakRef(this)
window.addEventListener("scroll", event => {
   ref.deref()?.handleScroll()
})
Enter fullscreen mode Exit fullscreen mode

This will allow the object to be garbage-collected, but will keep the event listener attached, meaning it will still fire on every scroll event, fail to deref the reference and therefore do nothing.

An easy way to clean up event listeners is to combine AbortController with FinalizationRegistry.

The former lets us pass a signal to an event that will remove the event, while the latter allows us to run some code when certain objects get collected.

The interface for this is relatively basic: We create a new FinalizationRegistry and pass it a callback. Then we register an object A and an associated (different) object B. When A gets garbage-collected, it obviously can't be passed to the callback, so instead, the callback is passed B.

const abortRegistry =
   new FinalizationRegistry(c => c.abort())
Enter fullscreen mode Exit fullscreen mode

This abortRegistry now allows us to register an object and an associated AbortController, and will call abort() on the controller whenever the object gets collected.

Now we just need to register our object on creation, and pass the controller's signal to the event listener.

Here's the complete code:

const abortRegistry =
   new FinalizationRegistry(c => c.abort())

class MyElement extends HTMLElement {
   constructor() {
      super()
      const ref =
         new WeakRef(this)
      const controller =
         new AbortController()
      abortRegistry.register(this, controller)

      window.addEventListener("scroll", event => {
         ref.deref()?.handleScroll()
      }, { signal: controller.signal })
   }

   handleScroll() {
      this.classList.toggle("top", window.scrollY == 0)
   }
}
Enter fullscreen mode Exit fullscreen mode

Comparing to lifecycle hooks

An easy point of criticism against the example above is that, whenever possible, resource cleanup should be achieved via the custom element API's lifecycle hooks connectedCallback() and disconnectedCallback().

Two reasons why this may not be a viable alternative are that

  1. This kind of cleanup might be necessary for objects that aren't DOM Elements and therefore cannot rely on their DOM connection for cleanup.

  2. There may be cases where it is necessary to continue sending signals to a custom element even while it is disconnected from the DOM, usually because it might be inserted again at a later point in time.

In either case, the only option for cleanup is keep the object set up until it actually becomes inaccessible and therefore couldn't possibly be needed anymore.


And that's it 😁

Did you learn something new? Was this old news to you? Let me know with a comment, and feel free to share how you handle cases like these in practice 👍


Cover Image: Jilbert Ebrahimi (Unsplash License)

Top comments (6)

Collapse
 
silverium profile image
Soldeplata Saketos

It looks pretty complicated to me.

This is something frameworks are helping devs to forget about. For instance, react has the useEffect that can be used to remove event listeners when the component is destroyed:

function ReactComponent() {
  React.useEffect(() => {
    window.addEventListener("scroll", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
    }
  }, []}
}
Enter fullscreen mode Exit fullscreen mode

In any case, using the FinalizationRegistry is not very reliable, according to the documentation developer.mozilla.org/en-US/docs/W...

So it's always better to clean listeners explicitly in the lifecycle of the object.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • Edited

There's a few problems I have with this.

For one, even with useEffect, you still have to write code to remove the event listener, so it's not like you can actually forget about it. It just takes a little bit less boilerplate, but that's just code size.

Also, custom elements can do this too. They have a connectedCallback and disconnectedCallback that you can actually rely on for cleanup if you need that.

And in fact, you could easily build a simple helper function like this.whileConnected(window, "scroll", onScroll) that adds an event listener and sets it up to be removed again, and it would be trivial to implement both on top of useEffect and custom element callbacks.

But most importantly,

this isn't about components. I used a custom element because it's a convenient example, but you may well need this sort of cleanup for a plain javascript object. Frameworks won't help you with that.

And lastly, although this is a matter of preference, I honestly find that the more imperative version using browser APIs looks a lot less complicated. It's a very step-by-step kind of setup that's a lot easier to follow than a function that takes a function which returns a function.


EDIT: It's also kinda boring to just have a framework take care of everything. Useful on the job, but dreadfully uninteresting to think about. It might be interesting how react et al. do these things under the hood, but just the usage isn't something I want to think or write about in my free time. Weak references and garbage collection callbacks are interesting. useEffect is not.

Collapse
 
silverium profile image
Info Comment hidden by post author - thread only accessible via permalink
Soldeplata Saketos

Come on...

You don't have to write code to remove the event listener. You can use nice hooks like the ones from usehooks-ts.com/react-hook/use-eve...

useEventListener('scroll', onScroll);
Enter fullscreen mode Exit fullscreen mode

It might be very interesting for devs to reinvent the wheel, but it's not efficient.

And yes, it is about components. A custom element is also a component. It's a matter of perspective and definition.

By the way, a function that returns a function that returns a function is something very common in programming languages. Have you ever heard about Functional Programming? Have you ever heard about functors? Time to learn!

 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Can you actually read?

You don't have to write code to remove the event listener

Yes, you literally did. And now your counter-argument seems to be that there's a library for that? Congratulations, you've proven the point I didn't even make.

And yes, it is about components.

I wrote it. Between you and me, one is qualified to say what that post is about, and the other one is you.

A custom element is also a component. It's a matter of perspective and definition.

Did you discover this recently, or why did you feel the need to point this out? Sorry, but this isn't news; the group of features that custom elements is a part of is literally called "web components". You're wrong about it being a matter of perspective. They just are.

I also point out how "but you may well need this sort of cleanup for a plain javascript object.", so I'm assuming you think {} is also a component? You can google why that's wrong, I'm not interested in explaining basics like that. Or maybe you just didn't understand even that simple sentence, which would probably be because you're not interested in having an honest discussion and just want to talk down to people.

By the way, a function that returns a function that returns a function is something very common in programming languages.

Yes, congratulations, you've discovered yet another super basic concept: higher order functions. Take a while to understand the basics, then come back and I can teach you about partial application, currying, closures, etc.

Also, I'm sorry to tell you, but FP is kind of old news? Maybe you discovered it recently and are all excited about it, but most of us have known that's a thing for years now; some even before the recent renewed interest in the style. If you haven't already, I suggest you also check out OOP; I personally don't like it as much, but it's interesting to read about. I'm sure there's lots for you to learn.


Setting the sarcasm aside though; I'm gonna be honest: I probably know more than you about most of the topics you've mentioned other than the frameworks which I don't use. I wrote a post about fundamentals because to some of us, understanding the basics is either just fun, or a necessity for performance reasons.

If you don't have any interest in lower level concepts, and are happy having the complexity taken away by a framework, all the power to you, but if you're going to go around trying to devalue content that doesn't pander to your specific tastes, I suggest unplugging your internet and keeping your toxic BS to yourself.

So with that being said; do you have anything of actual value to add to this conversation? Or are we done here?

Thread Thread
 
silverium profile image
Soldeplata Saketos

It might be toxic to you personally. I am sorry about that. I also feel sorry because you know so much more than me (and probably anyone reading any article you wrote) and I didn't realise because I don't care and none of the readers do.

When I said "yes, it's about components" I tried to nicely answer to your

But most importantly,
this isn't about components. I used a custom element because it's a convenient example, but you may well need this sort of cleanup for a plain javascript object. Frameworks won't help you with that.

Just like the rest of steps of my answer.

And there was a tiny bit of really valuable thing in my previous comment that you overlooked in your rageous read of my comment:

In any case, using the FinalizationRegistry is not very reliable, according to the documentation developer.mozilla.org/en-US/docs/W...

And I will paste it here so you can read it very well:

A conforming JavaScript implementation, even one that does garbage collection, is not required to call cleanup callbacks. When and whether it does so is entirely down to the implementation of the JavaScript engine. When a registered object is reclaimed, any cleanup callbacks for it may be called then, or some time later, or not at all.

Thread Thread
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

I'm not even going to address any of your BS anymore.

You've shown very clearly that you're not interested in having a constructive discussion and are just here to show how much better you are than me for not caring about browser APIs.

So to make this very clear, since you didn't pick up on it from my last message:

Leave me the fuck alone, I'm not interested in having this kind of conversation.

Some comments have been hidden by the post's author - find out more