Исходная проблема: T1051228 - Form - In some circumstances, browser hangs up when resizing if there is a DxForm with column adaptability
Проблема возникла из-за особенностей кода в dxEventEngine: иногда код зацикливается. Вкратце "если при выполнении обработчика события на это же событие добавляется новый обработчик" + еще несколько условий. Код dxEventEngine решили не менять: слишком большая вероятность что-то сломать в совершенно неожиданных местах.
Решили использовать setTimeout в коде компонента dxForm: https://github.com/DevExpress/DevExtreme/pull/20688/files
Я начал изучать эту проблему, я передавал ее ответственным за dxEventEngine, я получил ее обратно, изучил расклад и согласен, что изменение кода dxForm будет иметь меньше эффектов, чем изменение кода eventEngine.
Ребята предложили такое элементарное изменение:
instance.on('autoColCountChanged', function() { setTimeout(() => { that._refresh(); }, 0); });
Это изменение и правда элементарно, но эффекты от него посложнее:
- Самый частый эффект "забыть вызвать clearTimeout перед setTimeout" и получить зацикливание, когда метод вызывают, он добавляет свой вызов в очередь "setTimeout", отрабатывает, получает управление из очереди "setTimeout" и снова начинает работать, goto 1.
Для обработки этой ситуации нужен такой код:instance.on( 'autoColCountChanged', () => { if(this.autoColCountChangedTimeoutId) { clearTimeout(this.autoColCountChangedTimeoutId); this.autoColCountChangedTimeoutId = undefined; } this.autoColCountChangedTimeoutId = setTimeout( () => this._refresh(), 0 ); } );
- Второй эффект "забыть вызвать clearTimeout на dispose" и получить nullref, когда метод начнет работать с "дохлыми" свойствами "убитого" объекта, если после setTimeout но до начала выполнения элемента из очереди "setTimeout" был вызов dispose
Для этой ситуации нужны еще несколько строчек кода:_dispose: function() { if(this.autoColCountChangedTimeoutId) { clearTimeout(this.autoColCountChangedTimeoutId); this.autoColCountChangedTimeoutId = undefined; } this.callBase(); },
- Третий эффект "Breaking Change: изменение синхронного выполнения кода на выполнение когда-то потом через постановку в очередь", когда вызывающий код хочет обрабатывать результаты работы метода сразу после вызова метода: с вызовом setTimeout этих результатов не будет, ведь работа над ними еще не началась.
В моем случае такого ожидания вроде бы нет, потому что есть только один вызов без обращений к результатам работы кода:Но про остальной код вокруг этого метода я конечно же ничего не гарантирую._dimensionChanged: function() { if(this.option('colCount') === 'auto' && this.isCachedColCountObsolete()) { this._eventsStrategy.fireEvent('autoColCountChanged'); } },
Например, у меня сразу же упали тесты, которые как раз хотят получить результаты работы этого метода сразу же после вызова, и для них мне пришлось вписать emulatedTimer.tick(), что бы получить эти результаты.
Аналогичная ситуация может возникнуть и в клиентских приложениях, если будет несколько обработчиков события "dimensionChanged" после dxForm._dimensionChanged и они ожидают готовые результаты работы метода "_refresh()"
- Четвертый эффект "БЧ: для управления выполнением кода можно применить setTimeout только один раз", когда кто-то уже использовал setTimeout в своем коде, что бы выполнить свой код после "dxForm._refresh" и получить результаты его работы. После такого изменения вызов dxForm._refresh произойдет позже и клиентский код не получит эти результаты.
Тут я конечно же тоже ничего не гарантирую.
- Пятый эффект "невозможно написать тесты": я не понял как заставить наш eventEngine зациклиться при выполнении обработчиков, поэтому PR без тестов. Пишите, если у кого-то получилось написать такой тест.
Из-за этих эффектов я стараюсь не применять setTimeout для управления последовательностью выполнения кода: слишком много комбинаций.
Хотя иногда приходится делать решение именно на этом методе.
Комментариев нет:
Отправить комментарий