Игнорирует ли однопоточная эмуляция Blazor Server взаимодействие с JavaScript?

Сегодня я столкнулся с проблемой с компонентом, который оборачивает библиотеку JS, которая может повлиять на связанное значение компонента Blazor. Обновление значения параметра Mask приведет к многократному запуску кода JavaScript.

[Parameter]
public string Value { get; set; }

[Parameter]
public EventCallback<string> ValueChanged { get; set; }

[Parameter]
public Mask Mask { get; set; }

private Mask _mask;
private bool _maskNeedsUpdate;

protected override void OnParametersSet()
{
    // ... Handle other parameters.

    if (_mask != Mask)
    {
        _mask = Mask;
        _maskNeedsUpdate = true;
    }
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    // ... initialize JS mask on first render.

    if (_maskNeedsUpdate)
    {
        var valueAfterUpdate = await Js.InvokeAsync<string>("updateMask", _mask);
        if (valueAfterUpdate != Value)
        {
            await ValueChanged.InvokeAsync(valueAfterUpdate);
        }

        _maskNeedsUpdate = false; // <-- This causes problems.
    }
}

Это почти как если бы этот вызов ValueChanged.InvokeAsync() запускал параллельную повторную отрисовку, что приводило к постановке в очередь второго вызова взаимодействия JS, поскольку флаг все еще стоит true. Но я сомневаюсь в этом, поскольку буквально в следующей строке для него установлено значение false (между тем, все, что EventCallback.InvokeAsync(), кажется, делает, это по сути вызывает StateHasChanged() в родительском компоненте и вызывает его обратный вызов).

Изменение кода для установки флага перед вызовом JS, похоже, устранило проблему.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    // ... initialize JS mask on first render.

    if (_maskNeedsUpdate)
    {
        _maskNeedsUpdate = false; // <-- Toggle flag before awaiting JS.

        var valueAfterUpdate = await Js.InvokeAsync<string>("updateMask", _mask);
        if (valueAfterUpdate != Value)
        {
            await ValueChanged.InvokeAsync(valueAfterUpdate);
        }
    }
}

Однако это исправление меня смущает, потому что согласно документации:

Blazor использует контекст синхронизации (SynchronizationContext) для обеспечения единого логического потока выполнения. Методы жизненного цикла компонента и обратные вызовы событий, создаваемые Blazor, выполняются в контексте синхронизации.

Контекст синхронизации на стороне сервера Blazor пытается эмулировать однопоточную среду, чтобы максимально соответствовать модели WebAssembly в браузере, которая является однопоточной. Эта эмуляция ограничена только отдельной схемой, то есть две разные схемы могут работать параллельно. В любой момент времени внутри схемы работа выполняется ровно над одним потоком, что создает впечатление одного логического потока. Никакие две операции не выполняются одновременно в одной схеме.

Насколько я понимаю, однопоточная эмуляция не должна позволять моим компонентам Blazor «выполнять параллельный рендеринг», пока мой JavaScript все еще работает (и, если посмотреть на исходный код Blazor, в конечном итоге ожидается OnAfterRenderAsync()). Но тогда почему перемещение флага вверх до вызова взаимодействия JS решает мою проблему? Даже если родительский элемент выполняет повторный рендеринг, не помешает ли однопоточная эмуляция его параллельному повторному рендерингу?

Я хотел бы знать, следует ли мне следить за условиями гонки при разработке компонентов, которые вызывают взаимодействие JavaScript.

Приведенный выше пример был достаточно простым исправлением, но у меня есть другие компоненты, которые обертывают библиотеки JS, которые имеют гораздо более сложное поведение, где обе стороны C# и JS могут изменять связанные параметры, и «просто перемещение флага выше» не будет работать. для некоторых из них, если компонент может выполнять повторный рендеринг параллельно.

Вы пытаетесь применить маску, например, pattern к input? Возможно, покажите больше кода. Моя причина вопроса в том, что вам, возможно, не придется прибегать к JSInterop.

MrC aka Shaun Curtis 31.08.2024 16:45

@MrCakaShaunCurtis У меня есть некоторые компоненты, использующие маски, но есть и другие, которые охватывают более сложные библиотеки. Я использовал код маски в качестве примера, потому что он имеет самое простое решение. Нам еще предстоит найти, как исправить более сложные из них. Кроме того, мы используем библиотеку JS для маскировки наших входных данных в Blazor, потому что в противном случае вы получите плохой пользовательский интерфейс, когда символы появятся во входных данных до того, как обработчики событий Blazor сотрут их. Единственная альтернатива, которую мы нашли, — это сделать то же, что делает MudBlazor, перехватить ключевые события и передать их Blazor. Это не будет работать для разных типов компонентов.

micka190 31.08.2024 16:53

@MrCakaShaunCurtis (1/2) Извините за поздний ответ (у нас были длинные выходные). Ваш связанный пост действительно помог прояснить кое-что о том, когда срабатывает OnAfterRenderAsync (и как он может срабатывать несколько раз). Наши опасения по поводу исправления более сложных компонентов заключались в следующем: «Что произойдет, если возникнет несколько состояний гонки, когда JS завершает работу и запускает рендеринг, рендеринг уже происходит из-за изменения параметра, а пользователь вводит что-то, что ставит в очередь больше всего? Как мы справимся с этим в нашей собственной очереди?» Также помогло чтение о том, как работает контекст синхронизации Blazor. (Продолжение...)

micka190 03.09.2024 15:41

(2/2) В нашем случае, кажется, проблема в том, что EventCallback.InvokeAsync() вызывает постановку в очередь второго OnAfterRenderAsync(), но поскольку в нем есть своя async логика, флаг не обновляется до того, как второй начнет работать. Перемещение флага вверх, кажется, решает проблему, даже с более сложными компонентами. Спасибо за помощь!

micka190 03.09.2024 15:50
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
5
59
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Насколько я понимаю, однопоточная эмуляция не должна позволять моим компонентам Blazor «параллельно выполнять повторную визуализацию».

Это неправильное предположение. Асинхронность и многопоточность — это не одно и то же. Рендеринг Blazor является асинхронным (чередующимся), и да, новый рендеринг может произойти, когда вы выполняете ожидание где угодно. То, что все это происходит в одном потоке, не так важно.

Это описано в примечании в документации, в котором говорится:

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

Таким образом, в вашем исходном коде действительно было состояние гонки, и установка флага ранее действительно является решением.

но у меня есть другие компоненты, которые обертывают библиотеки JS и имеют гораздо более сложное поведение.

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

Ах, я видел часть «перед рендерингом компонента» и ошибочно предположил, что они имели в виду первоначальный рендеринг, а не повторный рендеринг. «Должна быть возможность перенести эту логику в OnParamsSet». У меня сложилось впечатление, что мы должны вызывать JS-код только из OnAfterRenderAsync(), чтобы избежать проблем, связанных с рендерингом элементов компонента (т. е. если повторный рендеринг приводит к устареванию ссылок на элементы).

micka190 31.08.2024 16:27

Кроме того, о бите Async и Multi-threaded: насколько я понимаю, асинхронный код использует базовый SynchronizationContext для определения того, как обрабатывать асинхронные операции, поэтому я подумал, что SynchronizationContext, пытающийся эмулировать однопоточную среду, будет транслироваться в асинхронные методы, работающие в похожая мода. Разве это не должно быть так?

micka190 31.08.2024 16:32

До последнего момента, да, вы можете просто думать об этом как об однопоточном. Но он по-прежнему асинхронен, поэтому рендеринг и обработчики событий могут и будут работать параллельно. Возможно, поищите статью Стивена Клири «нет нити». .

Henk Holterman 31.08.2024 17:50

Другие вопросы по теме