A glimpse into the mind of a JavaScript framework author

4 min read Original article ↗

Carl Mungazi

Press enter or click to view image in full size

Photo by Markus Spiske on Unsplash

Have you ever read the source code of a popular library and come across comments which explain what the code is doing? Sometimes the explanation given is straightforward but other times it leaves you scratching your head. I had one such moment recently and after much head scratching, it left me with a deeper appreciation of the effort that goes into open source tools and inspired me to write this post.

I came across the code in question whilst investigating how frontend frameworks attach event handlers to DOM elements. Over the last few months, I have been rebuilding different parts of the frontend stack as a way of improving my knowledge, and part of that has involved creating a UI framework based on the virtual DOM paradigm.

The code I was looking at was from Mithril. Internally, it registers events on DOM elements by creating an object and passing it as the second argument to the document.addEventListener method. The relevant source code is:

The third point: “The object does not inherit from Object.prototype, to avoid any potential interference with that (e.g. setters).” is what caught my attention. I reached out to Mithril core maintainer Isiah Meadows via the Mithril Gitter chat and his explanation is what we will go into next.

Guarding against rogue third-party code

Imagine that instead of inheriting from null, EventDict's prototype is Object.prototype. A Mithril user then writes the following code or uses a library which does the following:

The application is then written as follows:

If the form is submitted, I am on the setter will be logged to the console instead of I am on the form. The contents of the getter and setter functions are not important but their existence means the onsubmit event handler is not registered on the form as intended.

This is because when the event is triggered, the EventDict.prototype.handleEvent method is executed and this line var handler = this["on" + ev.type] returns the onsubmit function on Object.prototype instead of the function specified on the form element.

Get Carl Mungazi’s stories in your inbox

Join Medium for free to get updates from this writer.

The problem above also crops up when Mithril runs in an environment where Object.prototype has been extended in a much simpler fashion like so:

And the application code is:

In this example, we have added the onreset event to our form. For us to understand the problems it poses, we first have to look at Mithril's updateEvent method. This method runs whenever on-event handlers are being set or removed on DOM elements.

Virtual DOM frameworks like Mithril turn calls such as m('button', { type: 'reset'}, 'Reset') into objects which represent a given DOM element. In Mithril, these objects are called vnodes (for comparison, in React those objects are called fibers. I have written about them here). The key is the event name and the value is whatever object or function assigned to handle that event.

When the code is executed, the vnode object for the form element is passed to the updateEvent function. Inside the function, the else if clause runs because the vnode's events property is undefined. Once the function has finished executing, the vnode object will look like this:

The events property has been assigned an EventDict instance and that instance is then given a reference to the onreset function as one of its properties. Also, the form DOM element has an event listener attached for the onreset event. So far, so good.

updateEvent runs for every on-event present on a DOM element. The second time it is called, it is passed onsubmit as the key argument. This time, however, the first if clause runs because vnode.events is no longer null.

Within this if statement are two other if statements. The first one is skipped because vnode.events[key] is not equal to value, which in this case is the onsubmit function. In fact, the value of vnode.events[key] is 1. Why? Remember that this code is running in an environment where somebody has written this: Object.prototype.onsubmit = 1. Since the form EventDict instance does not yet have an onsubmit property and it inherits from Object.prototype, the JavaScript engine will walk up the prototype chain and find that onsubmit exists on Object.prototype.

The next if clause then checks that value has a function or object. This check passes but crucially, an event listener for the onsubmit event is not then attached to the form element because vnode.events[key] is not null as it should be, its value is 1. The next line adds the onsubmit property to the EventDict instance on the form element vnode along with the related function.

Like the first example where Object.defineProperty was used to add the onsubmit property to Object.prototype, when the form is submitted it will not behave as the developer intended.