65.9K
CodeProject 正在变化。 阅读更多。
Home

一个更简单的 Blazor Debounce

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2021 年 7 月 21 日

CPOL

4分钟阅读

viewsIcon

7386

两种方法及其在自动补全模式下的比较,该模式用于使用户更容易从长下拉列表中选择项目

An Easier Blazor Debounce

自动补全(也称为“先试输入”)是一种模式,用于使用户更容易从长下拉列表中选择项目。当用户键入时,会提供建议。这些通常是基于输入文本进行过滤的结果,但一些服务(如搜索引擎)也可能提供热门结果或基于您的历史记录的结果。如果您是Web开发人员,您可能已经实现了一些形式的自动补全。

顺便说一句,这篇文章的代码可以在以下存储库中找到

自动补全的一个问题是获取结果时的网络流量开销。为了提供即时反馈,您必须响应用户键入时的输入。由于网络延迟和查询开销,结果通常在用户已经修改过滤器后到达,因为他们的输入速度比结果返回的速度快。考虑这个输入字段

Enter search text:
<input @bind-value="Text"
@bind-value:event="oninput" />

一个天真或“经典”的获取实现如下所示

public string Text
{
    get => text;
    set
    {
        if (value != text)
        {
            text = value;
            InvokeAsync(async () => await SearchAsync(text));
        }
    }
}
private async Task SearchAsync(string text)
{
    if (!string.IsNullOrWhiteSpace(text))
    {
        foodItems = await Http.GetFromJsonAsync<FoodItem[]>(
        $"/api/foods?text={text}");
        calls++;
        totalItems += foodItems.Length;
        await InvokeAsync(StateHasChanged);
    }
}

咀嚼这个例子

在这个例子中,当用户输入时,输入被获取和更新。那么,有什么问题呢?为了测试这一点,我创建了一个服务器,该服务器执行内存搜索,但在提供结果之前引入了一秒的人工延迟。数据来自USDA食品数据库,其中包含许多简单的描述。大部分文本是重复的,但这对于我们的目的来说是可以的。我只是将标识符和描述文本保存在内存中。

这是控制器

[HttpGet]
public IEnumerable<FoodItem> Get([FromQuery] string text)
{
    IEnumerable<FoodItem> result = Enumerable.Empty<FoodItem>();
    if (text != null)
    {
        var safeText = text.Trim().ToLowerInvariant();
        if (!string.IsNullOrWhiteSpace(safeText))
        {
            result = FoodItems.Where(fi => fi.Description
            .Trim().ToLowerInvariant().Contains(safeText));
        }
    }
    Thread.Sleep(1000);
    return result;
}

🛑 让我们达成共识:我永远不会(永远)在我的生产服务器代码中放置 Thread.Sleep。这只是为了演示应用程序!

运行程序似乎工作正常,但我的测试用例产生了一些有趣的结果。如果我尽可能快地输入“oatmeal”,然后退格并输入“onions”,结果是 19 次调用,并返回了近 7000 个数据库项目。为了便于理解,只有 143 个项目匹配“oatmeal”,而 184 个项目匹配“onions”。显然,我们过度获取了。想象一下这种情况发生在数千个并发用户身上! 😱

这种方法也可能产生副作用。如果您在浏览器中打开网络选项卡,您将看到多个请求同时运行。如果出于某种原因,某个请求花费的时间比其他请求更长,它可能会乱序返回。想象一下输入“oatmeal”然后输入“onions”并接收到 oatmeal 的结果是多么令人困惑!

完美的时间

一个常见且完全可以接受的解决方案是添加一个计时器来对输入进行去抖动。逻辑如下所示

1) key press detected
2) timer running? shut it down
3) set a timer for 300 milliseconds
4) key press in under 300ms? Go back to (1)
5) timer fires and processes fetch

这在代码中的实现如下

private string Text
{
    get => text;
    set
    {
        if (value != text)
        {
            text = value;
            DisposeTimer();
            timer = new Timer(<span style="color:#40a070">300);
            timer.Elapsed += TimerElapsed_TickAsync;
            timer.Enabled = true;
            timer.Start();
        }
    }
}
private async void TimerElapsed_TickAsync(
object sender,
EventArgs e)
{
    DisposeTimer();
    await SearchAsync(text);
}

它运行得很好。您可以快速键入,并且只有在您暂停或停止时才会获取结果。使用与之前相同的场景,输入“oatmeal”然后更正为“onions”只需进行 2 次调用并获取总共 327 个项目。这正是我们正在寻找的两组结果。

如果您喜欢计时器方法,我们就完成了。无需继续阅读。但是,我意识到有一种更简单的模式,它用一些额外的调用和获取来换取不需要跟踪和处理计时器的代码。

一个更简单的去抖动

让我们从逻辑开始

1) key press detected
2) already loading a result?
3) yes - set the queued flag
4) no - reset the queued flag
5) fetch the items
6) if the queued flag is set, reset it and go to (5)
7) done

基本上,发生的事情是我们在服务器允许的范围内快速获取。与经典示例不同,没有同时获取。它总是按顺序获取,并且始终返回最相关的结果。

这是代码:

private string Text
{
    get => text;
    set
    {
        if (value != text)
        {
            text = value;
            InvokeAsync(async () => await SearchAsync(text));
        }
    }
}
private async Task SearchAsync(string text)
{
    if (!string.IsNullOrWhiteSpace(text))
    {
        if (loading)
        {
            queued = true;
            return;
        }
    do
    {
        loading = true;
        queued = false;
        foodItems = await Http.GetFromJsonAsync<FoodItem[]>(
        $"/api/foods?text={text}");
        calls++;
        totalItems += foodItems.Length;
        loading = false;
    }
    while (queued);
    await InvokeAsync(StateHasChanged);
    }
}

要理解这是如何工作的,请想象我输入“oatmeal”并发生以下情况

1) key press detected "o"
2) nothing queued, set start to load "o" items
3) key press detected "a"
4) items still not back, so set queued flag
5) key press detected "t"
6) items still not back, so set queued flag
7) items return, queued flag is checked so another fetch is issued
8) the fetch returns with items matching "oat"

令人困惑的部分是看到 queued 设置为 false 然后根据 queuedtrue 重复。请记住,这是异步代码。当代码等待 fetch 时,相同的代码可能会为下一次按键执行,并且该代码将 queued 设置回 true

这种折衷方案导致 4 次调用返回 1806 个项目。我们仍然过度获取,但它要少得多。

以下是方法的比较

Autocomplete comparison

为了让您更容易自己查看,我创建了一个小应用程序。

自动补全笼中格斗

代码在此存储库中

它实现了三种不同的自动补全方法。这是我的测试运行的结果

Autocomplete cage match

您可以自己查看代码并进行测试。我欢迎您的想法和反馈!

© . All rights reserved.