Select选择器的实现原理与前端优化实战经验
先看效果,再看代码
做前端这些年,Select 组件看似简单,但真要写得顺手、兼容性好、还能支持搜索和异步加载,其实挺折腾的。我一开始也直接用原生 <select>,结果产品说“能不能加个搜索”“能不能支持多选”“能不能懒加载”,行吧,原生方案直接出局。
现在我基本都用 Ant Design 的 Select(React 项目)或者 Element Plus 的 ElSelect(Vue 项目),但有些轻量级场景,自己封装一个反而更灵活。下面这段代码是我最近在某个内部工具里写的,亲测有效,支持远程搜索、防抖、loading 状态,还带点小优化:
import { useState, useEffect, useMemo } from 'react';
function RemoteSearchSelect({ value, onChange, placeholder = '请选择' }) {
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const fetchData = async (keyword) => {
setLoading(true);
try {
const res = await fetch(https://jztheme.com/api/users?keyword=${encodeURIComponent(keyword)});
const data = await res.json();
setOptions(data.map(item => ({ label: item.name, value: item.id })));
} catch (err) {
console.error('加载失败', err);
setOptions([]);
} finally {
setLoading(false);
}
};
// 防抖处理
useEffect(() => {
const timer = setTimeout(() => {
if (searchText.trim() !== '') {
fetchData(searchText);
} else {
setOptions([]);
}
}, 300);
return () => clearTimeout(timer);
}, [searchText]);
const handleSearch = (text) => {
setSearchText(text);
};
return (
<Select
showSearch
value={value}
placeholder={placeholder}
onSearch={handleSearch}
onChange={onChange}
loading={loading}
options={options}
filterOption={false} // 关键!禁用默认过滤
notFoundContent={loading ? '加载中...' : '无匹配数据'}
/>
);
}
这里注意下,filterOption={false} 这一行很重要。如果不关掉,AntD 会用你本地 options 做二次过滤,而你 options 是空的(初始状态),就会搜不到东西。我踩过好几次坑,调试半天才发现是这个配置的问题。
这个场景最好用:动态联动 Select
比如省市区三级联动,或者“先选分类再选子项”这种需求。很多人一上来就搞三个 Select 嵌套,状态管理乱成一锅粥。我的做法是:用一个自定义 Hook 把逻辑抽出来,主组件只管渲染。
// useCascaderSelect.js
import { useState, useCallback } from 'react';
export const useCascaderSelect = (fetchLevel) => {
const [level1, setLevel1] = useState([]);
const [level2, setLevel2] = useState([]);
const [selected1, setSelected1] = useState(null);
const [selected2, setSelected2] = useState(null);
const loadLevel1 = useCallback(async () => {
const data = await fetchLevel(1);
setLevel1(data);
}, [fetchLevel]);
const handleLevel1Change = useCallback(async (val) => {
setSelected1(val);
setSelected2(null);
const data = await fetchLevel(2, val);
setLevel2(data);
}, [fetchLevel]);
return {
level1,
level2,
selected1,
selected2,
loadLevel1,
handleLevel1Change,
setSelected2,
};
};
然后在组件里:
function CategorySelector() {
const {
level1, level2, selected1, selected2,
loadLevel1, handleLevel1Change, setSelected2
} = useCascaderSelect(async (level, parentId) => {
const params = parentId ? ?parentId=${parentId} : '';
const res = await fetch(https://jztheme.com/api/categories${params});
return res.json();
});
useEffect(() => {
loadLevel1();
}, [loadLevel1]);
return (
<div>
<Select
value={selected1}
options={level1}
onChange={handleLevel1Change}
placeholder="请选择一级分类"
/>
<Select
value={selected2}
options={level2}
onChange={setSelected2}
placeholder="请选择二级分类"
disabled={!selected1}
/>
</div>
);
}
这样拆开之后,逻辑清晰多了,测试也好写。而且如果以后要加第三级,直接在 Hook 里扩展就行,不用动 UI 组件。
踩坑提醒:这三点一定注意
- 虚拟滚动别乱开:如果你选项超过 1000 条,确实该上虚拟滚动(比如 AntD 的
virtual属性)。但要注意,开启后某些样式会失效,比如你自定义的 option 样式可能被覆盖。我上次为了加个图标,折腾了两小时才发现是虚拟滚动的容器限制了子元素高度。 - 受控组件的 value 类型要一致:比如你后端返回的是数字 ID,但 Select 的 value 你传了字符串,那选中状态就对不上。这种情况我建议统一转成字符串处理,或者用严格相等判断。别问我怎么知道的,反正控制台没报错,UI 就是不显示选中,查到怀疑人生。
- 移动端点击穿透问题:在某些低端安卓机上,Select 弹出层关闭后,底下的按钮会被误触发。解决方案是在弹层关闭时加个
setTimeout延迟 300ms 再允许交互,或者用 CSS 的pointer-events: none临时禁用。虽然有点 hack,但有效。
高级技巧:自定义 Option 渲染
有时候产品要展示更多信息,比如用户头像+名字+部门。这时候就得用自定义 Option。AntD 和 Element 都支持,但写法略有不同。以 AntD 为例:
const CustomOption = ({ user }) => (
<div style={{ display: 'flex', alignItems: 'center', padding: '5px 12px' }}>
<img src={user.avatar} alt="" width="24" height="24" style={{ marginRight: 8 }} />
<div>
<div>{user.name}</div>
<div style={{ fontSize: 12, color: '#999' }}>{user.department}</div>
</div>
</div>
);
// 在 Select 中使用
<Select
options={users.map(u => ({
label: <CustomOption user={u} />,
value: u.id,
// 注意:搜索时还是要提供纯文本
title: ${u.name} - ${u.department}
}))}
showSearch
filterOption={(input, option) =>
option.title.toLowerCase().includes(input.toLowerCase())
}
/>
关键点在于:自定义 label 用于展示,但搜索还得靠 title 或者其他纯文本字段。否则你搜“技术部”,根本搜不到,因为 React 元素没法被 indexOf 匹配。
最后说两句
Select 看似基础,但细节特别多。从性能优化到交互体验,再到各种边界情况(比如空数据、加载失败、键盘导航),每个点都能挖出坑来。我上面给的方案也不是完美的——比如远程搜索那个,其实应该加个缓存避免重复请求,但项目赶进度就先这么用了,反正用户搜同一个词的概率不高。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多(比如结合 Tag 实现标签选择、拖拽排序等),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流,一起少走弯路。
