搜索功能实现遇到的那些坑

大家好,我是前端西瓜哥,今天我们来聊聊搜索的一些坑。

搜索是一个比较常见的业务需求,但里面有些容易踩坑的地方,我们今天来聊一聊。

我们先用 React 实现一个简单的搜索 Demo。

当我们在 input 输入内容时,就会通过 onChange 事件触发请求,将返回结果保存到 resulte 变量并输出到页面上。

// 模拟网络请求
const getSearchResult = (keyword) => {
  // 假设网络良好,稳定 150ms
  const wait = 150;
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(keyword);
    }, wait);
  });
};

function App() {
  const [result, setResult] = useState('');

  const searchKeyword = (e) => {
    // 发送搜索请求
    const keyword = e.target.value;
    getSearchResult(keyword).then((res) => {
      setResult(res);
    });
  };

  return (
    <div className="App">
      <input autoFocus onChange={searchKeyword} />
      <div>搜索结果:{result}</div>
    </div>
  );
}

上面的实现有个问题:如果用户连续输入内容,会导致在短时间内发送大量请求给后端,对服务端造成不小压力

这里其实很多请求都是没用的,只有最后一个才是有用的。

我们可以做一下优化。

防抖

首先我们要对请求做 防抖,就是要用户在停止输入后再等待特定的时间,才发送请求。如果在这段时间内用户再次输入了内容,则重新开始等待

假设我们 1s 中执行了 6 次函数,它本来应该是下面这样子的:

12 3            456 
----------------------

添加防抖能力后,我们让函数某次执行后特定的时间没有新的触发,才真正去执行,结果是我们只执行了 2 次:

// 防抖后
         3           6
----------------------

使用防抖后,我们就可以让用户疯狂输入过程中的请求不能真正发起,当用户停止输入后才真正发送请求,从而降低服务端压力。

我们对发送请求的 searchKeyword 函数做一个防抖。

import { useDebounceFn } from 'ahooks';

const { run: searchKeyword } = useDebounceFn(
  (e) => {
    // 发送搜索请求
    const keyword = e.target.value;
    getSearchResult(keyword).then((res) => {
      setResult(res);
    });
  },
  {
    wait: 200
  }
);

这里用了 ahooks 的 useDebounceFn 对函数做了防抖,设置等待时间为 200 ms。

useDebounceFn 底层用了 lodash.debouce,并配合 useRef 确保返回的函数引用不变。

你可能奇怪为什么不直接用 debouce,其实这是有原因的。

因为 React 的函数组件 发生状态更新会重新执行函数组件,如果直接用 debounce 方法,每次其实都是生成了一个全新的加了防抖特性的新函数,导致前后多个 onChange 事件触发的是多个独立的函数,最终结果是发送请求数量和触发事件树相同。

我们看下加了防抖的效果,可以看到中间一些像是 1234 的请求被丢弃掉了,确实减少了不要的网络请求。

图片

debouce-search

有人说能不能用节流。不推荐,因为用节流的话,用户在持续输入的过程中,还是发送了一些无意义的请求,只是频率比直接请求低了一些罢了。

如果你是使用关键词联想推荐,则可以使用节流。这种方案发起请求其实是在用户回车或点击 “搜索” 按钮触发了,和本文讨论的场景不同。

上一个请求结果覆盖下一个的问题

看起来貌似没啥问题了,但其实我们还忽略了一个问题,就是当网络不稳定的场景。

假设内容为 1 时发送了一个请求 A,然后内容变成 12 又发送了一个请求 B,然后停止输入。

因为网络不稳定,请求 B 先返回了,页面显出出了 12 对应的结果,这没问题。但过了一会,1 的结果接着返回了结果。

此时,你就会看到,明明搜索栏输入的是 12,返回的却是 1 的结果。

为此,我们需要 丢弃最后一个请求之前的所有请求

我们可以用闭包的方式维护一个请求对应的 currReqId ,并维护一个全局变量 lastReqId 记录最后请求的 id。

当返回请求结果时,如果 currReqId 和 lastReqId 相同才继续执行接下来的逻辑;如果不等,就丢弃。

// 最后请求 id
const lastReqId = useRef({});

const { run: searchKeyword } = useDebounceFn(
  (e) => {
    const currReqId = {};
    lastReqId.current = currReqId;
    // 发送搜索请求
    const keyword = e.target.value;
    getSearchResult(keyword).then((res) => {
      // “当前请求 id” 和 “最后一次请求的 id” 相同时才继续接下里的逻辑
      if (lastReqId.current === currReqId) {
        console.log('发送返回结果', res);
        setResult(res);
      }
    });
  },
  {
    wait: 200
  }
);

这里我用空对象来作为请求的 id,是因为每次声明 {} 都指向一个新的内存地址,可以确保每个请求 id 都不相等。你也可以用一个自增数字来做 id,只要确保唯一即可。

还有一种方式就是使用中止 Web 请求的 API:AbortController。它能真正地中止 HTTP 请求,相比 id 对比的方式,能真正地减少网络消耗。

id 对比的方式请求还是在持续的,只是返回的请求不使用而已。

但考虑到浏览器兼容性,不要太依赖 AbortController,可以同时使用 id 对比策略和 AbortController。

完整线上 Demo:

https://codesandbox.io/s/whw2q1

结尾

总结一下,对于输入过程中就请求搜索结果的场景,我们需要做两个特殊处理:

  1. 使用防抖,减少一些不必要的请求;

  2. 将最后一次请求之外的请求结果丢弃,防止前一次请求结果覆盖掉后一次。

我是前端西瓜哥,欢迎关注我,学习更多前端知识。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340