An Exploration In Separating Hash-Change Events From Programmatic Hash Updates
A couple of weeks ago, I presented the idea of learning about "best practices" in event-driven programming by looking at the way the browser handles user-generated vs. programmatic updates. As an example, I put forth the Form-submission. If the user submits a form, the browser triggers a "submit" event; if, however, the page programmatically submits the form, no such "submit" event is triggered. This kind of separation between what does and does not trigger an event can be seen in several such user-interface (UI) interactions. Putting my faith in the years of development behind the Web Browser, I wanted to see if I could apply this same kind of "best practice" mentality to the window's hash value.
NOTE: I know there are a number of really awesome plugins that abstract hash-functionality. This is, in no way the point of this blog post. I say this only to keep you focused on what's important. If you recommend a plugin, just keep in mind that you are definitely going to be missing the point of the following exploration.
Well, what about the browser's location? This is definitely a UI element; after all, the user can manually change it. Going on what we've established previously, this means that a manual change of the location should trigger an event while a programmatic change of the location should not trigger anything.
Before we can do this, I think we need to create a sense that a UI element consists of two components: an internal model and an external manifestation. An event, therefore, gets triggered when the external manifestation is altered before the internal model is updated. If the internal model is updated first (such as through a programmatic change), the external manifestation is merely changed in order to align its state with that of the internal model and no event need be triggered.
When it comes to the window's location, the external manifestation (the Location bar) has already been created for us. As such, we need to create our own version of the internal model. This internal model, however, necessarily needs to be something more than a process that just monitors the state of the location - that's just an observer, not a model. What we need is something that actually contains an internal state that describes the location.
Once we have this internal model, we can then use it to both respond to UI (Location bar) changes and to update the UI based on the changes to this model. After some playing around, here's what I came up with:
As you can see, I create a Singleton - hashController - that has two public methods: getHash() and setHash(). Internally, the hashController does have a process that monitors the window's location. This is to see when its "external manifestation" has been updated by the user - user-driven updates, do trigger a "hashchange" event. However, if we programmatically call the setHash() method, you'll notice something critical - the very first thing that the hashController does is stop monitoring its external manifestation.
I stop the monitoring process for two reason:
I am honestly not sure what kind of race-conditions a function running within a setInterval() process might create. I don't understand the parallel nature of timers enough to know how to most carefully treat shared variables.
To make a point. When we programmatically alter the internal state of the hashController, I am trying to be absolutely clear that we are going to update the internal state and then, only as a subsequent action, change the external manifestation (Location bar) for no reason other than to mirror the new internal state of the model.
In performing the updates in this order, once the monitoring process is turned back on, there's no difference between the internal model and the external manifestation. As such, the monitoring process has no reason to trigger any event.
I don't know if this sounds completely crazy or what, but there's something about this that feels very right to me. But, then again, I'm not exactly versed in the art of event-driven programming. Of course, this type of approach doesn't necessarily apply to all aspects of event-driven programming; this exploration deals very specifically with models that can be changed by direct user interactions; specifically, models that have user-interface components.
Want to use code from this post? Check out the license.
It appears that no race conditions really exist. While the timers allow things to begin executing in parallel, it appears that the actual execution is done in serial.