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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With