Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check for async function re-entry in JS

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).

like image 264
Dinu Avatar asked Nov 06 '22 13:11

Dinu


1 Answers

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/

like image 113
Tarun Lalwani Avatar answered Nov 15 '22 11:11

Tarun Lalwani