Check for async function re-entry in JS - javascript

Scenario:
We have a MutationObserver handler function handler.
In handler, we do some DOM manipulation that would trigger handler again. Conceptually, we would have a reentrant handler call. Except MutationObserver doesn't run in-thread, it will fire after the handler has already finished execution.
So, handler will trigger itself, but through the async queue, not in-thread. The JS debugger seems to know this, it will have itself as an async ancestor in the call stack (i.e. using Chrome).
In order to implement some efficient debouncing of events, we need to detect same; that is, if handler was called as a result of changes triggered by itself.
So how to do?
mutationObserver=new MutationObserver(handler);
mutationObserver.observe(window.document,{
attributes:true,
characterData:true,
childList:true,
subtree:true
});
var isHandling;
function handler(){
console.log('handler');
// The test below won't work, as the re-entrant call
// is placed out-of-sync, after isHandling has been reset
if(isHandling){
console.log('Re-entry!');
// Throttle/debounce and completely different handling logic
return;
}
isHandling=true;
// Trigger a MutationObserver change
setTimeout(function(){
// The below condition should not be here, I added it just to not clog the
// console by avoiding first-level recursion: if we always set class=bar,
// handler will trigger itself right here indefinitely. But this can be
// avoided by disabling the MutationObserver while handling.
if(document.getElementById('foo').getAttribute('class')!='bar'){
document.getElementById('foo').setAttribute('class','bar');
}
},0);
isHandling=false;
}
// NOTE: THE CODE BELOW IS IN THE OBSERVED CONTENT, I CANNOT CHANGE THE CODE BELOW DIRECTLY, THAT'S WHY I USE THE OBSERVER IN THE FIRST PLACE
// Trigger a MutationObserver change
setTimeout(function(){
document.getElementById('asd').setAttribute('class','something');
},0);
document.getElementById('foo').addEventListener('webkitTransitionEnd',animend);
document.getElementById('foo').addEventListener('mozTransitionEnd',animend);
function animend(){
console.log('animend');
this.setAttribute('class','bar-final');
}
#foo {
width:0px;
background:red;
transition: all 1s;
height:20px;
}
#foo.bar {
width:100px;
transition: width 1s;
}
#foo.bar-final {
width:200px;
background:green;
transition:none;
}
<div id="foo" ontransitionend="animend"></div>
<div id="asd"></div>
Note
Our use case comprises of 2 components here; one we will call contents which is any run-of-the-mill web app, with a lot of UI components and interface. And an overlay, which is the component observing the content for changes and possibly doing changes of its own.
A simple idea that is not enough is to just disable the MutationObserver while handling; or, assume every second call to handler as recursive; This does not work in the case illustrated above with the animationend event: the contents can have handlers which in turn can trigger async operations. The two most popular such issues are: onanimationend/oneventend, onscroll.
So the idea of detecting just direct (first-call) recursion is not enough, we need quite literally the equivalent of the call stack view in the debugger: a way to tell if a call (no matter how many async calls later) is a descendant of itself.
Thus, this question is not limited to just MutationObserver, as it necessarily involves a generic way to detect async calls descendent of themselves in the call tree. You can replace MutationObserver with any async event, really.
Explanation of the example above: in the example, the mutationobserver is triggering the bar animation on #foo whenever #foo is not .bar. However, the contents has an transitionend handler that sets #foo to .bar-final which triggers a vicious self-recursion chain. We would like to discard reacting to the #foo.bar-final change, by detecting that it's a consequence of our own action (starting the animation with #foo.bar).

One possible workaround for this could be to stop the mutation observer when one mutation is being fired
mutationObserver=new MutationObserver(handler);
mutationObserver.observe(window.document,{
attributes:true,
characterData:true,
childList:true,
subtree:true
});
// Trigger a MutationObserver change
document.getElementById('foo').setAttribute('class','bar');
document.getElementById('foo').setAttribute('class','');
function handler(){
console.log('Modification happend')
mutationObserver.disconnect();
// Trigger a MutationObserver change
document.getElementById('foo').setAttribute('class','bar');
document.getElementById('foo').setAttribute('class','');
mutationObserver.observe(window.document,{
attributes:true,
characterData:true,
childList:true,
subtree:true
});
}
See the JS fiddle
https://jsfiddle.net/tarunlalwani/8kf6t2oh/2/

The way I have done this in the past is to create semaphore to flag when an async function has already been called and is waiting execution on the event loop.
Here is a simple example for requestAnimationFrame
import raf from 'raf'
import State from './state'
let rafID
function delayedNotify () {
rafID = null
State.notify()
}
export default function rafUpdateBatcher () {
if (rafID) return // prevent multiple request animation frame callbacks
rafID = raf(delayedNotify)
}
In this case once you call the function, all future calls will be ignored until the first one is executed. Think of it as an async throttle function
For more complex scenarios then another solution might be this project
https://github.com/zeit/async-sema

From what I gather from reading your comments, if action A triggered action B asynchronously, you want to be able to tell where action A was done (in general, not just in a mutation observer). I don't think there's any trick built into JavaScript to do this like it seems you're looking for, however, if you know exactly how your JavaScript works, you can track this information. Job queues in JavaScript are FIFO by definition, the event loop queue also works this way. That means you can store information corresponding to a specific event in an array at the same time you're doing the action which triggers the event, and be confident that they're getting processed in the same order as the array. Here's an example with your mutation observer.
const
foo = document.getElementById('foo'),
mutationQueue = [];
function handler(){
console.log('handler');
const isHandling = mutationQueue.shift();
if(isHandling){
console.log('Re-entry!');
// Throttle/debounce and completely different handling logic
return;
}
setTimeout(()=> {
foo.setAttribute('class','bar');
foo.setAttribute('class','');
mutationQueue.push(true);
}, 1000 * Math.random());
}
mutationObserver=new MutationObserver(handler);
mutationObserver.observe(foo, {
attributes:true,
characterData:true,
childList:true,
subtree:true
});
function randomIntervals() {
setTimeout(()=>{
foo.setAttribute('class','bar');
foo.setAttribute('class','');
mutationQueue.push(false);
randomIntervals();
}, 1000 * Math.random())
}
randomIntervals();
<div id='foo'></div>
You have to make sure you add the appropriate value to the array at every point in your code that is going to trigger your async handler, or the entire thing will be messed up. I've never done this myself, I just thought of it for this question, but it seems pretty easy to do wrong. However, I fear this may be the only way to do what you want in general.

Related

Prevent other observers from being called with Prototype.js

I have two observers registered via the Event.observe function of the Prototype JavaScript framework. Both observers observe the same event on the same element. The second observer is registered by a 3rd party library and it performs the real event handling. The first observer is my own observer. I want to check some condition inside and if the condition is met, I want to prevent the second observer from being called. Is there a way to achieve this? I have tried event.stop() and return false and it does not have the effect I am looking for.
element.observe(
"myevent",
function (event) {
if (/* some condition*/) {
/* the following attempts do not work really */
event.stop();
return false;
}
});

An efficient way to cancel a mouseover event... read on to see why

I'm in the process of authoring a completely client side web language reference site. A problem that I encountered today; I have a side panel that is a unordered list of terms and they have onmouseover event listeners. I decided it would be a good idea to add a delay prior to execution and cancel the event at run-time if the mouse was no longer over that element. This is what I've come up with but I feel there must be a better way.
var currentXCoordinate=0
var currentYCoordinate=0
var elementFromCurrentMousePosition=0
function trackCurrentMousePosition(event) {
if (document.elementFromPoint(event.clientX, event.clientY).nodeName=="SPAN") {
elementFromCurrentMousePosition=document.elementFromPoint(event.clientX, event.clientY).parentNode
}
else {
elementFromCurrentMousePosition=document.elementFromPoint(event.clientX, event.clientY)
}
return (currentXCoordinate=event.clientX, currentYCoordinate=event.clientY, elementFromCurrentMousePosition)
}
function initPreview(event, obj) {
arg1=event
arg2=obj
setTimeout("setPreviewDataFields(arg1, arg2)", 100)
}
function setPreviewDataFields(event, obj) {
if ('bubbles' in event) {
event.stopPropagation()
}
else {
event.cancelBubble=true
}
if (elementFromCurrentMousePosition!=obj) {
return 0;
}
The code goes on to do all the wonderful stuff I want it to do if execution wasn't cancelled by the previous if statement. The problem is this method is seeming to be really processor intensive.
To sum it up: on page load all my event listeners are registered, cursor position is being tracked by a onmousemove event. Applicable list items have a onmouseover event that calls the initPreview function which just waits a given period of time before calling the actual setPreviewDataFields function. If at run-time the cursor is no longer over the list element the function stops by return 0.
Sadly that's the best I could come up with. If anyone can offer up a better solution I would be very grateful.
Why not just use mouseout to tell when the mouse leaves an element? Running all of that code every time the mouse moves isn't ideal.
Also, you really shouldn't pass a string to setTimeout like that. Instead, pass a function. As a bonus, you can get rid of those evil global variables arg1 and arg2. With those being globals, I think you will run into issues if init gets called again before the timeout expires.

Could this cause a JavaScript memory leak?

I use a couple of functions heavily in my code. Now, as I'm looking for the source of high memory usage, I want to ensure that they're not the culprits.
Using jQuery, I bind and trigger custom events on the body element. When an event is triggered, I store it in a list of triggered events. One of the helper functions I use is called "waitfor". Here's some pseudocode:
waitfor = function(event, callback){
if(event_has_ever_been_called){
callback(); //RUN IMMEDIATELY
}
else{
//BIND CALLBACK TO RUN AS SOON AS THE EVENT IS TRIGGERED
$("body").bind(event, function(){
callback();
});
}
}
For example,
//ADD GOOGLE MAP TO PAGE
... listen for the google map 'idle' event,
... then call $("body").trigger("gmap.ready")
//ADD MARKERS AS SOON AS POSSIBLE (BUT NOT BEFORE)
waitfor("gmap.ready", function(){
//add markers
});
This seems very straightforward to me but I'm a little concerned that it (or any of my other functions that use anonymous callback functions) could be causing high memory usage.
Is this sufficient information to determine that this function is safe / not safe?
You should call one instead of bind to remove the handler after the event fires.
Otherwise, your function, and everything referenced by its closure, will stay referenced forever by jQuery's handler list.

Safely using hook events in JavaScript

In source code here
http://www.daftlogic.com/sandbox-javascript-slider-control.htm
There is these instructions:
// safely hook document/window events
if (document.onmousemove != f_sliderMouseMove) {
window.f_savedMouseMove = document.onmousemove;
document.onmousemove = f_sliderMouseMove;
}
I don't understand what it does and why it would be safer to do that this way, does someone understand?
It might be that some other code already assigned an event handler to document.onmousemove. The problem with this method, as opposed to addEventListener, is that only one function can be assigned to element.onXXXX. Thus, if you blindly assign a new event handler, an already existing one might be overwritten and other code might break.
In such a case, I would write:
if (document.onmousemove) {
(function() {
var old_handler = document.onmousemove;
document.onmousemove = function() {
old_handler.apply(this, arguments);
f_sliderMouseMove.apply(this, arguments);
};
}());
}
else {
document.onmousemove = f_sliderMouseMove;
}
This way it is ensured that both event handlers are executed. But I guess that depends on the context of the code. Maybe f_sliderMouseMove calls window.f_savedMouseMove anyway.
It is just saving the current hook, presumably so it can call it at the end of its own hook method.
It avoids stamping on some other codes hook that was already set up.
You would expect the hook code to be something like:
f_sliderMouseMove = function(e) {
// Do my thing
// Do their thing
window.f_savedMouseMove();
}
[obligatory jquery plug] use jquery events and you can ignore problems like this...
It appears that this code is storing the function that is currently executed on a mouse move, before setting the new one. That way, it can presumably be restored later, or delegated to, if need be. This should increase compatibility with other code or frameworks.

jQuery temporary unbinding events

Maybe I'm totally missing something about even handling in jQuery, but here's my problem.
Let's assume there are some event binding, like
$(element).bind("mousemove", somefunc);
Now, I'd like to introduce a new mousemove binding that doesn't override the previous one, but temporarily exclude (unbind) it. In other words, when I bind my function, I must be sure that no other functions will ever execute for that event, until I restore them.
I'm looking for something like:
$(element).bind("mousemove", somefunc);
// Somefunc is used regularly
var savedBinding = $(element).getCurrentBinding("mousemove");
$(element).unbind("mousemove").bind("mousemove", myfunc);
// Use myfunc instead
$(element).unbind("mousemove", myfunc).bind("mousemove", savedBindings);
Of course, the somefunc is not under my control, or this would be useless :)
Is my understanding that is possible to bind multiple functions to the same event, and that the execution of those functions can't be pre-determined.
I'm aware of stopping event propagation and immediate event propagation, but I'm thinking that they are useless in my case, as the execution order can't be determined (but maybe I'm getting these wrong).
How can I do that?
EDIT: I need to highlight this: I need that the previously installed handler (somefunc) isn't executed. I am NOT defining that handler, it may be or may be not present, but its installed by a third-party user.
EDIT2: Ok, this is not feasible right now, I think I'm needing the eventListenerList, which is not implemented in most browsers yet. http://www.w3.org/TR/2002/WD-DOM-Level-3-Events-20020208/changes.html
Another way could be to use custom events, something along these lines:
var flag = 0;
$(element).bind("mousemove", function() {
if(flag) {
$(this).trigger("supermousemove");
} else {
$(this).trigger("magicmousemove");
}
}).bind("supermousemove", function() {
// do something super
}).bind("magicmousemove", function() {
// do something magical
});
$("#foo").click(function() {
flag = flag == 1 ? 0 : 1; // simple switch
});
Highly annoying demo here: http://jsfiddle.net/SkFvW/
Good if the event is bound to multiple elements:
$('.foo').click(function() {
if ( ! $(this).hasClass('flag')) {
do something
}
});
(add class 'flag' to sort of unbind, add it to 'bind')

Categories

Resources