Firstly, you ask:
why is hashchange
called at the first time of the first execution? Shouldn't this script change only the hash without any alert?
To answer this, we can delve into the specification. When navigating to a new fragment (ie setting document.location.hash
), the specification goes through a number of steps, one of which is:
- Traverse the history to the new entry, with the asynchronous events flag set. This will scroll to the fragment identifier given in what is now the document's address.
The specification for traversing the history goes on to say:
- If the asynchronous events flag is not set, then run the following steps synchronously. Otherwise, the asynchronous events flag is set; queue a task to run the following substeps.
- If state changed is true, fire a trusted event with the name popstate at the Window object of the Document, using the PopStateEvent interface, with the state attribute initialized to the value of state. This event must bubble but not be cancelable and has no default action.
- If hash changed is true, then fire a trusted event with the name hashchange at the browsing context's Window object, using the HashChangeEvent interface, with the oldURL attribute initialized to old URL and the newURL attribute initialized to new URL. This event must bubble but not be cancelable and has no default action.
So all of that taken together means that when you run your code, the event listener for hashchange
will be added before the code in the substeps of step 14 is run and will subsequently be fired when the hash is set.
How can I fix it so that it works as described?
To fix it, you can also queue the adding of your event listener using setTimeout(.., 0)
:
setTimeout(function() {
$(window).on('hashchange', function() {
alert('hello');
});
}, 0);
Since you add this to the queue after setting the hash, it will be added to the queue after the task queued in step 14 above, and so the event listener only gets added after the event has been fired.