Сценарий:
У нас есть функция-обработчик MutationObserver
handler
.
В handler
мы делаем некоторые манипуляции с DOM, которые снова вызовут handler
. Концептуально у нас должен быть реентерабельный вызов handler
. За исключением того, что MutationObserver
не запускается в потоке, он сработает после того, как handler
уже завершит выполнение.
Итак, handler
сработает сам, но через асинхронную очередь, а не в потоке. Отладчик JS, кажется, знает об этом, он будет иметь асинхронного предка в стеке вызовов (т.е. с использованием Chrome).
Чтобы реализовать эффективное устранение дребезга событий, нам нужно обнаружить одно и то же; то есть, если handler
был вызван в результате изменений, инициированных им самим.
Итак, как это сделать?
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>
Примечание Наш сценарий использования состоит из двух компонентов. один из них мы назовем contents. Это любое заурядное веб-приложение с большим количеством компонентов пользовательского интерфейса и интерфейса. И наложение, которое является компонентом, отслеживающим контент на предмет изменений и, возможно, вносящим изменения самостоятельно.
Простая идея, которой недостаточно, — просто отключить MutationObserver
во время обработки; или предположим, что каждый второй вызов handler
является рекурсивным; Это не работает в показанном выше случае с событием animationend
: содержимое может иметь обработчики, которые, в свою очередь, могут запускать асинхронные операции. Два самых популярных таких выпуска: onanimationend
/oneventend
, onscroll
.
Таким образом, идея обнаружения только прямой (первого вызова) рекурсии недостаточна, нам нужен буквально эквивалент представления стека вызовов в отладчике: способ определить, является ли вызов (независимо от того, сколько асинхронных вызовов позже) потомок самого себя.
Таким образом, этот вопрос не ограничивается только MutationObserver
, поскольку он обязательно включает общий способ обнаружения асинхронных вызовов, являющихся потомками самих себя в дереве вызовов. На самом деле вы можете заменить MutationObserver
любым асинхронным событием.
Объяснение приведенного выше примера: в примере мутационный наблюдатель запускает анимацию bar
на #foo
всякий раз, когда #foo
не является .bar
. Однако contents имеет обработчик transitionend
, который устанавливает #foo
в .bar-final
, что запускает порочную цепочку саморекурсии. Мы хотели бы отказаться от реакции на изменение #foo.bar-final
, обнаружив, что это следствие нашего собственного действия (запуск анимации с #foo.bar
).
new WeakSet()
, куда вы добавляете измененные элементы и проверяете позже. - person wOxxOm   schedule 26.09.2019handler
, или из-за него, или и того, и другого. То, что вы говорите, похоже, не добавляет к тому, что уже делаетMutationObserver
: подготовьте пакет изменений для асинхронной обработки; Мне нужно больше, мне нужно знать, откуда взялись эти модификации. Мне нужно то, что отладчик, похоже, делает безупречно: определить,handler is on the async call stack.
- person Dinu   schedule 26.09.2019handler
всегда будет в асинхронной задаче, MutationEvents — это микрозадачи, как и Promises. - person Kaiido   schedule 26.09.2019handler
раньше был в асинхронном стеке. - person Dinu   schedule 26.09.2019MutationObserver
к тому, что делает содержимое. Таким образом, он будет обрабатывать изменения, сделанные содержимым, которые я буду называть внутренними изменениями. Но при их обработке он может манипулировать содержимым, генерируя внеплановые изменения, которые могут запустить порочный круг бесконечной рекурсии. - person Dinu   schedule 26.09.2019MutationObserver
с 2 обратными вызовами, которые взаимно отключаются во время наблюдения (поэтому обработчик одного наблюдателя отключит другого наблюдателя во время обработки). Но мне было интересно, может ли быть более общий способ сделать это независимо от генератора асинхронных событий (здесь мы могли бы заменитьMutationObserver
наrequestAnimationFrame
илиonanimationend
, и это все тот же вопрос: как определить, срабатывает ли обработчик асинхронно. - person Dinu   schedule 26.09.2019