Проверить повторный вход асинхронной функции в JS

Сценарий:

У нас есть функция-обработчик 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).


person Dinu    schedule 26.09.2019    source источник
comment
Почему бы просто не использовать функцию более высокого порядка для устранения дребезга? Вам не нужно реализовывать логику устранения дребезга в каждой отдельной функции, которую вы создаете.   -  person VLAZ    schedule 26.09.2019
comment
Строго говоря, в JavaScript нет такого понятия, как повторный вход. То, что вы видите, — это просто еще один цикл цикла событий, в частности, цикл очереди микрозадач. В JavaScript нет специальных механизмов, поэтому вы просто устанавливаете контрольное свойство Expando для элемента или используете new WeakSet(), куда вы добавляете измененные элементы и проверяете позже.   -  person wOxxOm    schedule 26.09.2019
comment
@VLAZ - я не совсем уверен, что следую за тобой.   -  person Dinu    schedule 26.09.2019
comment
@wOxxOm - если я правильно понимаю, что вы говорите, - мне нужно было бы сказать, изменился ли элемент в результате handler, или из-за него, или и того, и другого. То, что вы говорите, похоже, не добавляет к тому, что уже делает MutationObserver: подготовьте пакет изменений для асинхронной обработки; Мне нужно больше, мне нужно знать, откуда взялись эти модификации. Мне нужно то, что отладчик, похоже, делает безупречно: определить, handler is on the async call stack.   -  person Dinu    schedule 26.09.2019
comment
handler всегда будет в асинхронной задаче, MutationEvents — это микрозадачи, как и Promises.   -  person Kaiido    schedule 26.09.2019
comment
@Kaiido - верно, так как мне это проверить? Этот handler раньше был в асинхронном стеке.   -  person Dinu    schedule 26.09.2019
comment
Каков ваш практический случай, когда вы будете это делать? Допустим, вы хотите мутировать из состояния A в состояние B в обработчике, затем при втором вызове обработчика вы уже будете в состоянии B, после чего больше не реагируете.   -  person Kaiido    schedule 26.09.2019
comment
@Kaiido - Для описания более высокого уровня - у меня есть приложение, состоящее из двух частей: одну часть я назову contents, которая является базовым полнофункциональным приложением, и часть, которую я назову overlay< /b>, который в основном представляет собой интерфейс редактирования/манипулирования и имеет отзывчивое поведение через MutationObserver к тому, что делает содержимое. Таким образом, он будет обрабатывать изменения, сделанные содержимым, которые я буду называть внутренними изменениями. Но при их обработке он может манипулировать содержимым, генерируя внеплановые изменения, которые могут запустить порочный круг бесконечной рекурсии.   -  person Dinu    schedule 26.09.2019
comment
@Kaiido - Итак, мне нужно отличить внутриполосные изменения от внеполосных (наложения). Лучшее, что я могу придумать, это использовать 2 MutationObserver с 2 обратными вызовами, которые взаимно отключаются во время наблюдения (поэтому обработчик одного наблюдателя отключит другого наблюдателя во время обработки). Но мне было интересно, может ли быть более общий способ сделать это независимо от генератора асинхронных событий (здесь мы могли бы заменить MutationObserver на requestAnimationFrame или onanimationend, и это все тот же вопрос: как определить, срабатывает ли обработчик асинхронно.   -  person Dinu    schedule 26.09.2019
comment
@Dinu Я думаю, вы могли бы получить лучшие ответы, если бы людям не нужно было читать комментарии, чтобы понять ваш истинный вопрос.   -  person Mason    schedule 05.10.2019
comment
@Mason Если в моем вопросе есть что-то неясное или оно каким-то образом обманчиво, что это?   -  person Dinu    schedule 05.10.2019
comment
@Dinu, похоже, у вас есть конкретный вопрос о том, как определенным образом обрабатывать наблюдателя мутаций, но похоже, что ваш вопрос на самом деле более широк и касается обработки событий в целом.   -  person Mason    schedule 05.10.2019


Ответы (3)


Одним из возможных обходных путей для этого может быть остановка наблюдателя мутаций, когда запускается одна мутация.

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
});
}

См. скрипт JS

https://jsfiddle.net/tarunlalwani/8kf6t2oh/2/

person Tarun Lalwani    schedule 28.09.2019
comment
Спасибо, мы это учли (см. комментарии к вопросу). Но мы надеялись найти решение, позволяющее определять, когда обработчик сам срабатывает; потому что в нашем вопросе MutationObserver можно заменить на requestAnimationFrame, onanimationend, даже setTimeout... результат тот же: создание потока асинхронных задач. Таким образом, простое отсоединение MutationObserver только создает видимость решения проблемы. Пример: если обработчик устанавливает анимированный класс CSS для элемента, имеющего обработчик onanimationend, который, в свою очередь, запускает мутацию... вы получаете картину. - person Dinu; 28.09.2019
comment
И это становится еще сложнее, если использовать API для управления любым компонентом пользовательского интерфейса: многие из них используют, например, setTimeout(...,0) для поэтапных операций; в этом случае у меня снова остается асинхронный вызов, с которым отключение/включение наблюдателя не поможет, поскольку выполнение запланировано асинхронно. Но глядя на трассировку стека в браузере, это легко обнаружить, поскольку JS по-прежнему отслеживает handler как предка асинхронного вызова. - person Dinu; 28.09.2019
comment
Не используйте трассировку стека браузера, потому что браузер обычно имеет расширенную отладочную информацию, которую вы не можете получить в самом javascript. - person Tarun Lalwani; 29.09.2019
comment
Я знаю, я надеялся, что есть какая-то структура, такая как асинхронная область, которую я мог бы использовать... Я почти уверен, что для работы await должна быть такая вещь... которая позволила бы установить переменную, которая будет доступны в будущих асинхронных задачах. Я знаю, что это второстепенное использование, поэтому я просто ловлю кого-то, кто может просто знать трюк :) - person Dinu; 29.09.2019

В прошлом я делал это так, чтобы создать семафор для отметки, когда асинхронная функция уже была вызвана и ожидает выполнения в цикле обработки событий.

Вот простой пример для 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)
}

В этом случае после вызова функции все будущие вызовы будут игнорироваться до тех пор, пока не будет выполнен первый. Думайте об этом как об асинхронной дроссельной функции.

Для более сложных сценариев другим решением может быть этот проект https://github.com/zeit/async-sema

person David Bradshaw    schedule 03.10.2019
comment
Хорошо, но у меня нет события «конец», которое является временным и не связано со стеком вызовов. Я также не хочу увольнять все события, которые происходят во временном интервале. Я хочу уволить (или по-другому обработать) только события, которые происходят как асинхронная рекурсия моих собственных действий в обработчике. Не любое другое событие, возникающее в результате основного наблюдаемого приложения. - person Dinu; 04.10.2019
comment
Затем вам нужно поэкспериментировать с подъемом семафора, когда ваше событие завершено, либо с setTimeout 0, либо с тем, что не работает, попробуйте установить атрибут данных в элементе HTML и использовать мутациюObserver, чтобы определить, когда он удален. - person David Bradshaw; 05.10.2019

Из того, что я понял из ваших комментариев, если действие A вызвало действие B асинхронно, вы хотите иметь возможность сказать, где было выполнено действие A (в общем, а не только в наблюдателе мутаций). Я не думаю, что в JavaScript есть какая-то хитрость, позволяющая сделать то, что вы ищете, однако, если вы точно знаете, как работает ваш JavaScript, вы можете отслеживать эту информацию. Очереди заданий в JavaScript по определению являются FIFO, очередь циклов событий также работает таким же образом. Это означает, что вы можете хранить информацию, соответствующую определенному событию, в массиве одновременно с выполнением действия, которое запускает событие, и быть уверенным, что они обрабатываются в том же порядке, что и массив. Вот пример с вашим наблюдателем мутаций.

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>

Вы должны убедиться, что добавили соответствующее значение в массив в каждой точке вашего кода, которая будет запускать ваш асинхронный обработчик, иначе все будет испорчено. Я никогда не делал этого сам, я просто подумал об этом для этого вопроса, но кажется, что это довольно легко сделать неправильно. Однако я боюсь, что это может быть единственным способом сделать то, что вы хотите в целом.

person Mason    schedule 05.10.2019
comment
Хм, после первого вызова handler() ваша мутацияQueue всегда будет [true], так как первое false извлекается, и никакое другое значение никогда не вставляется в него... - person Dinu; 05.10.2019
comment
@Dinu Я только что скопировал твой код, я подумал, что ты собираешься сделать что-то в разделе if (isHandling), что сделает его пригодным для использования. Если нет, вы можете просто сделать это оператором if...else. - person Mason; 05.10.2019
comment
Я имею в виду, когда isHandling никогда не бывает правдой? С вашим кодом каждый второй вызов handler, независимо от того, откуда он вызывается. - person Dinu; 05.10.2019
comment
@Dinu, разве ты не хочешь такого поведения? Что вы хотите, так это иметь возможность сказать, когда изменения были сделаны внутри функции handler, не так ли? В вашем примере код handler вносит свои изменения каждый раз, когда изменения вносятся из-за пределов обработчика. - person Mason; 05.10.2019
comment
Кроме того, я пропустил это утверждение return;, поэтому я предложил if...else, забудьте, что я это сказал. - person Mason; 05.10.2019
comment
@Dinu Я уверен, что этот подход сработает, если вы дадите мне конкретный пример для запуска во фрагменте, я изменю его на него. - person Mason; 05.10.2019
comment
Что ж, кажется, ваш код работает только потому, что только в этом очень ограниченном примере каждый второй вызов обработчика является рекурсивным вызовом. Ему вообще не нужна очередь, вы могли бы просто использовать счетчик и протестировать вызовы #even... Это синонимично другим ответам, которые предлагают отключить наблюдателя при обработке нашей собственной версии использования двух наблюдателей мутаций в тандеме. . Однако в этой последовательности не всегда вызывается handler; если в процессе есть какой-либо промежуточный асинхронный шаг (например, handler вызывается в результате события animationend, изменяющего DOM), он не будет работать. - person Dinu; 05.10.2019
comment
См. также stackoverflow.com/questions/58118551/: я надеюсь, что это объяснение также прояснит, почему этот вопрос о MutationObserver обязательно касается асинхронных событий в целом; обнаружения прямой рекурсии 1-го уровня недостаточно. - person Dinu; 05.10.2019
comment
Да, событие animationend в базовом интерфейсе, за которым я наблюдаю, может (и почти всегда будет, это основная причина события animationend) изменить DOM, тем самым вызвав цепную реакцию. Кроме того, многие компоненты пользовательского интерфейса используют setTimeout(0) в своих собственных обработчиках событий для подготовки событий, поэтому, если в handler я отключу какой-либо обработчик contents (например, onscroll, который приходит на ум), велика вероятность того, что об асинхронных последствиях (поскольку onscroll почти всегда отвергается). - person Dinu; 05.10.2019
comment
@Dinu Я только что изменил его, чтобы показать, что он работает с кучей случайных асинхронных операций. - person Mason; 05.10.2019
comment
Я не могу добавить mutationQueue.push(true); к (виртуальному) коду setTimeout; задается приложением content; Если бы я мог изменить все приложение content, где бы оно ни выполняло какие-либо изменения DOM, мне бы вообще не понадобился наблюдатель. Я бы просто позвонил handler() прямо оттуда. Но увы, я не могу; content уже существует и имеет свои обработчики, которые могут запускать асинхронные события. - person Dinu; 05.10.2019
comment
Таким образом, перефразируя, асинхронный код, который вы добавили в handler в setTimeout (представление правильное), не будет в handler, а будет кодом, который уже существует в content, например animationend, onscroll и все виды других событий и что я не могу все изменить. Таким образом, он НЕ будет содержать требуемый код установки флага. Мне нужно обнаружить это поведение только с помощью кода вне setTimeout. - person Dinu; 05.10.2019
comment
Я обновил вопрос более наглядным примером (и это также не засорит вашу консоль). - person Dinu; 05.10.2019