This is the theory part, and the next part is the code part.

preface

At work, we register event listeners for Windows, DOM nodes, WebSoket, or simply event centers.

// window
window.addEventListener("message".this.onMessage);
// WebSoket
socket.addEventListener('message'.function (event) {
    console.log('Message from server ', event.data);
});
// EventEmitter
emitter.on("user-logined".this.onUserLogined);
Copy the code

If it is not removed, it may cause a memory leak.

SPA exacerbates this phenomenon. For example, after the React component is loaded, the listener event is registered on the Window, and the component is uninstalled but not deleted. It is likely to snowball out of control.

componentDidMount(){
    window.addEventListener("resize".this.onResize);
}
componentWillUnmount(){
  // Forget remove EventListener
}
Copy the code

Our topic today is analyzing event listening and troubleshooting memory leaks that may result from it.

This paper mainly discusses and analyzes several technical points:

  1. How do I know exactly if an object or function is recycled
  2. The nature of common event listening functions
  3. How do I collect DOM event listener functions
  4. Common way to intercept methods
  5. Weak reference collection problem
  6. How to determine event listener function duplication

Results demonstrate

Alarm, high-risk event statistics, event statistics and other functions, let’s have a look at the effect.

warning

When you register an event, if you find an event listener with the same attribute, you should report it to the police. Four with:

  1. Same event dependent object

    Such asWindow.SocketEquivalent to an instance
  2. Event type, for examplemessage.resize.play, etc.
  3. Event callback function
  4. Event callback function options

Screenshots from my analysis of the actual project, message event added repeatedly, warning!!

At high risk of statistics

High – risk statistics is to the early warning, it will count the four same attributes of the event monitoring. Is the most important way to detect leaks in the event callback function.

DOM events

The screenshots are from my analysis of the actual projectOn the window objectMessage repeated addition of messagesThe number of times is up to 10

EventEmitter module

The screenshots are from my analysis of the actual project ,APP_ACT_COM_HIDE_Series events are added repeatedly

Event statistics

Lists all event-listening callbacks by type, including the function name or the function body. It is also extremely useful for us to analyze the problem.

So,

Everybody, everybody, everybody, do give the function a name, do give the function a name, do give the function a name. .

At this point, names really matter !!!!!!!

How do I know exactly if an object or function is recycled

An intuitive and effective answer: Weak reference WeakRef + GC(garbage collection)

Why weak references? Because we can’t recycle objects because of our analysis and statistics? Otherwise the analysis would be wrong.

A weak reference

WeakRef is introduced by ES2021 to create a weak reference to an object directly without preventing the original object from being scavenged by garbage collection.

WeakRef instance objects have a deref() method that returns the original object if it exists; This method returns undefined if the original object has been cleared by the garbage collection mechanism.

let target = {};
let wr = new WeakRef(target);

let obj = wr.deref();
if (obj) { // Target is not cleared by garbage collection
  // ...
}
Copy the code

Let’s look at a practical example:

The left target will not be collected, the right target will be collected.

Seeing this, you should have at least two senses:

  1. window.gc()What the hell is

    This is the method provided by V8, which actively triggers garbage collection, as discussed next.
  2. IIFEThe use of this closure does reduce variable contamination and leakage

The garbage collection

Garbage collection is periodic. Take Chrome as an example, it can actively perform garbage collection. If the object that was supposed to be recycled is still around after the voluntary recycling operation, it is likely to cause a leak.

How can a V8-based browser proactively perform garbage collection? The answer is: Change chrome’s startup parameters and add –js-flags=”– expose-GC”

After that, you can just call the GC method directly for garbage collection

summary

With WeakRef + active GC, you can test where you think there may be leakage or contamination and troubleshoot problems.

See the essence through the appearance

Appearance of event listening

Returning to the topic, our focus today is on event callback functions. Our common representations of event subscription types in Web programming are:

  • DOM events

    Mainly DOM2 level events, i.eaddEventListener.removeEventListener
  • WebSocket.socket.io.ws.mqttAnd so on this

In essence, there are two kinds:

  • Eventtarget-based event subscription

Window, Document, body, div, all of those common DOM-related elements, XMLHttpRequest, WebSocket, AudioContext, and so on, essentially inherit from EventTarget.

  • EventEmitter based event subscription

    mqttwsIs based onevents 。

    The famoussocket.ioIs based oncomponent-emitter.

    What they all have in common is passingonandoffEtc to subscribe and cancel events.

So it’s easy to listen for and collect information about subscribing and unsubscribing events.

Nature – the prototype

The essence of both the EventTarget and EventEmitter series is to subscribe and unsubscribe on instances after they are instantiated.

Instantiation, for better reuse and less memory overhead, tends to put common methods on prototype, and yes, the essence of the problem goes back to prototype.

So, event callback collection is messing with the native, rewriting the subscribed and unsubscribed methods on the prototype, in two points:

  1. Collect event listening information
  2. Keep original function

Further nature is method interception, so let’s approach method interception together

Methods intercept

Several methods of interception

Method interception, I have collected and sorted out about 7 ways, of course some similar, the problem is not big:

  • Simply rewrite the original method
  • A simple agent
  • Inheritance way
  • A dynamic proxy
  • ES6+ standard Proxy
  • ES5 standard defineProperty
  • ES6+ standard decotator

Simple examples of each method can be found in the way these methods are intercepted

Ideal and universal are the last three, of course,

  • Decotator is obviously not a good fit here, one is that the decorator requires explicit invasion code, and the other is that it costs too much.

  • DefineProperty is a very straightforward and efficient way to redefine get and return our modified function. But, I don’t, I don’t, I like to play Proxy.

  • Proxy

One: Proxy, which returns a new object that you need to use in order to effectively Proxy. The second is that we are responsible for supporting restore, so it is more accurate to use a cancelable proxy. The simple code looks like this:

const ep = EventTarget.prototype;
const rvAdd = Proxy.revocable(ep.addEventListener, this.handler);
ep.addEventListener = rvAdd.proxy;
Copy the code

How do I collect DOM event listener functions

We intercept the prototype methods, essentially to collect event listeners. In addition to intercepting prototypes, there are ways to get them.

Third-party librariesgetEventListeners

It just directly modifies the prototype method and stores the relevant information on the nodes, which works and is not recommended.

disadvantages

  1. Every node was hacked, and event information was kept on the node
  2. Only one element can be fetched at a time

The getEventListeners method of the Chrome console gets individual Node events 

disadvantages

  1. Only available on the console
  2. Only one element can be retrieved at a time

Chrome Console, Elements => Event Listeners

  1. Can only be used in the developer tools interface
  2. It’s a lot of trouble to find

Chrome More Tools => Performance Monitor Can collect THE NUMBER of JS Event listeners

There are no details, just one statistic

Data structures and weak references

WeakRef was mentioned earlier, but we need to think about which objects we need to weakreference.

What data structure is selected for storage

Since we are doing statistics and analysis, we must store some data. Here we need to use objects as keys, because we’re counting event subscriptions for instances of EventTarget or EventEmitter.

So the preferences, Map, Set, WeakMap, WeakSet, who do you choose? If you do, try to do something about it. If you do, try to do something about it. If you can’t traverse, of course you can’t do statistics.

Therefore, it is appropriate to choose Map here.

Weak references to those data

Let’s start by listing the data that needs to be designed for event subscriptions and unsubscriptions:

window.addEventListener("message".this.onMessage, false);
emitter.on("event1".this.onEvent1);
Copy the code

Comparison code analysis:

  1. target

    Objects mounted by events, such as EventEmitter, are examples
  2. type

    The event type
  3. listener

    Monitoring function
  4. options

    Options, even though EventEmitter doesn’t have them, none of them, we’ll just take them as they are.

Weak references to target and listener are selected, and the general data storage structure is as follows.

We weakly reference the event dependent body and the event callback function, which TypeScript says:

interface EventsMapItem {
    listener: WeakRef<Function>;
    options: TypeListenerOptions
}

Map<WeakRef<Object>, Record<string, EventsMapItem[]>>
Copy the code

It seems OK, in fact, there is a serious problem, with the support of the program, the number of Map keys will increase, this Key is WeakRef, WeakRef weak reference object may have been recycled, and the WeakRef associated with tartget will not be recycled.

Of course, you can periodically clean up, or ignore the Key of WeakRef that has no real reference when traversing.

But not friendly! Here, we welcome the next protagonist, FinalizationRegistry

FinalizationRegistry

FinalizationRegistryObject allows you to request a callback when the object is garbage collected.

Here’s a simple example:

const registry = new FinalizationRegistry(name= > {
    console.log(name,  "It was recycled.");
});
var theObject = {
    name: 'Test object',
}
registry.register(theObject, theObject.name);
setTimeout(() = > {
    window.gc();

    theObject = null;
}, 100);
Copy the code

After an object is collected, the output message should be theObject = null, so it is never a bad habit to set theObject to null after it is determined that theObject is not in use.

We use FinalizationRegistry to listen for the collection of event dependent objects, and the code looks like this:

  #listenerRegistry = new FinalizationRegistry<{ weakRefTarget: WeakRef<object> }>(
    ({ weakRefTarget }) = > {
      console.log("evm::clean up ------------------");
      if(! weakRefTarget) {return;
      }
      this.eventsMap.remove(weakRefTarget);
      console.log("length", [...this.eventsMap.data.keys()].length); })Copy the code

It is not difficult to understand that Map key is WeakRef, so after target is reclaimed, we need to delete the associated WeakRef.

At this point, we can collect information about the object and its registered event listeners. With this data, the next step is to alert, analyze, and count.

How do I identify repeated event listeners

Eventtarget-based event subscription

First let’s take a look at some code. Please consider that after a button is clicked, it outputs several clicks:

<button id="btn1"</button>function onClick(){
    console.log("clicked");
}
const btnEl = document.getElementById("btn");

btnEl.addEventListener("click", onClick);
btnEl.addEventListener("click", onClick);
btnEl.addEventListener("click", onClick);
Copy the code

The answer is: once

Because EventTarget has a natural ability to de-weight, see multiple identical event handlers

You might say you get it, let’s make it a little harder, how many times is it now?

    btnEl.addEventListener("click", onClick);
    btnEl.addEventListener("click", onClick, false);
    btnEl.addEventListener("click", onClick, {
        passive: false}); btnEl.addEventListener("click", onClick, {
        capture: false}); btnEl.addEventListener("click", onClick, {
        capture: false.once: true});Copy the code

Answer: Once again

The criteria for determining whether the callback function is the same is: the value of capture in options is the same. The default value for capture is false.

Because of this feature, we need to be judicious when intercepting subscription functions so as not to collect them by mistake.

If addEventListener returns a Boolean value, it can be used as a basis for judgment. Unfortunately, it returns undefined, providence, laugh, do not cry.

By this point, some people should be laughing, isn’t this not repeated? So why leak??

The root source of the leak

I mentioned at the beginning that SPA exacerbates this phenomenon, which is the memory leak caused by event functions.

// Hooks have the same problem
componentDidMount(){
    window.addEventListener("resize".this.onResize);
}
componentWillUnmount(){
  // Forget remove EventListener
}

Copy the code

This.onresize is created with the component, so every time the component is created, it is also created. The code is the same, but it is still a new one. When a component is destroyed, this. OnResize is referenced by the window and cannot be destroyed.

The same is true for EventEmitter based event functions. If you’re in the habit of typing logs, you’ll notice that logs are coming out like crazy, and you’re lucky you found a leak.

What is the same function

How to judge the same function becomes our key.

The same is true, of course, for eventTarget-based events, which are naturally shielded, while EventTarget-based events are not so lucky.

Now, the method that everyone sees every day, but also ignores, toString, yes, it’s him, it’s him, it’s him, our lovely little toString.

function fn(){
    console.log("a");
}
console.log(fn.toString()) 
// Output: :
// function fn(){
// console.log("a");
/ /}
Copy the code

If you remember Bob’s SeaJS, which relies on lookup, it uses toString

We compare content, the vast majority of cases are no problem, except:

  1. Your function code is exactly the same

    There is a rule in ESLint that seems not to be usedthisClass, should not be written in class. Indeed, you should think about the code implementation.
  2. Built-in function
const random = Math.random
console.log("name:", random.name, ",content:", random.toString())
// name: random ,content: function random() { [native code] }
Copy the code
  1. The bind function
function a(){
    console.log("name:".this.name)
}

var b = a.bind({})
console.log("name:", b.name, ",content:", b.toString())
// name: bound a ,content: function () { [native code] }
Copy the code

So we examine the built-in functions and functions that come after bind. The basic idea is name and {[native code]}.

A big problem no, a big problem, which means that the functions we bind can’t be compared, so we can’t determine if they’re the same.

Rewrite the bind

The answer is to rewrite bind so that it returns functions with attributes pointing to the original function, and if there’s a better way, please let me know.

function log(this: any) {
    console.log("name:".this.name);
}

var oriBind = Function.prototype.bind;
var SymbolOriBind = Symbol.for("__oriBind__");
Function.prototype.bind = function () {
    var f = oriBind.apply(this as any, arguments as any);
    f[SymbolOriBind] = this;
    return f;
}

const boundLog: any = log.bind({ name: "Ha ha" });
console.log(boundLog[SymbolOriBind].toString());

//function log() {
// console.log("name:", this.name);
/ /}

Copy the code

If you rewrite bind, there will be more instability elements, so:

  1. WeakRef is also used to reference and reduce unstable psychology
  2. Override bind is not enabled by default, and the basic problem check is almost complete. Then enable the override bind option, and only analyze the event listener function after being bind.

How do I know if it’s a function after bind or one of the functions mentioned above

  1. The function name, whose name isBound [original function name]
  2. The body of the function,{ [native code] }

summary

The basic idea

  1. WeakRefSet up andtargetThe association of objects does not affect their collection
  2. rewriteEventTargetEventEmitterTwo series of subscribing and unsubscribing related methods that collect event registration information
  3. FinalizationRegistry listeningtargetRecycle, and clean up related data
  4. Function alignment, in addition to reference alignment, also has content alignment
  5. For functions that follow BIND, override the BIND method to retrieve the original method code content

Two concerns

  1. Compatibility yes, it can only be used for debugging on newer browsers. But, no problem! Found and fixed, low version high probability fixed.

  2. How to debug mobile terminal is not the focus of this article.

After the analysis of the above several problems, we are done with only the east wind.

Stay tuned for the code article

Write in the last

Please go to the technical exchange groupCome here,. Or add my wechat Dirge-Cloud and take me to learn together.