介绍
最近在开发的时候,需要前端单独实现数据的筛选。
写完觉得挺有意思的,所以就抽出来单独写了一个DEMO,分享给大家,也欢迎大家指正。
效果
DEMO可以通过码上掘金体验 多条件筛选 - 码上掘金 (juejin.cn)。
代码
主要函数就三个,分别是:
筛选条件判断函数
目前支持的是包含/不包含/等于/不等于四种筛选条件。
function handleConditions(type = "include", leftVal, rightVal) {
if (type === "equal") {
return leftVal === rightVal;
}
if (type === "not-equal") {
return leftVal !== rightVal;
}
if (type === "include") {
return `${leftVal}`.indexOf(rightVal) !== -1;
}
if (type === "not-include") {
return `${leftVal}`.indexOf(rightVal) === -1;
}
}
数据交集函数
是一个纯函数,用于筛选出符合条件的所有数据,多个条件取交集。
第一次数据筛选和后续数据筛选使用的数据源要区分开,因为第一次筛选的数据源是原始数据,后续筛选的数据源是经过第一次筛选后的数据。
function intersectionFun(data, conditions) {
let tmp = [];
if (conditions.length === 0) {
tmp = data;
} else {
for (let index = 0; index < conditions.length; index++) {
const { type, key, value } = conditions[index];
if (index === 0) {
tmp = data.filter((item) => {
return handleConditions(type, item[key], value);
});
} else {
tmp = tmp.filter((item) => {
return handleConditions(type, item[key], value);
});
}
}
}
return tmp;
}
数据并集函数
是一个纯函数,用于筛选出符合条件的所有数据,多个条件取并集。
需要注意取并集后的数据会存在重复,需要去重。
function unionFun(data, conditions) {
let tmp = [];
if (conditions.length === 0) {
tmp = data;
} else {
for (let index = 0; index < conditions.length; index++) {
const { type, key, value } = conditions[index];
tmp = tmp.concat(
data.filter((item) => {
return handleConditions(type, item[key], value);
})
);
}
}
return [...new Set(tmp)];
}
界面
DEMO用到的元素也不多,所以使用的是原生HTML实现的界面,比较简陋(这个时候 UI 框架的作用就体现出来了😄😄😄)。
body {
max-width: 980px;
margin: 20px auto;
}
.form-wrap {
display: flex;
justify-content: space-around;
}
.form-item {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
button {
margin-right: 5px;
}
table {
border-collapse: collapse;
}
<form class="form-wrap" method="post" onsubmit="search(event)" onreset="resetForm()">
<div class="form-item">
<label for="id">编码:</label>
<input type="text" id="id" name="id" placeholder="请输入内容" />
</div>
<div class="form-item">
<label for="name">姓名:</label>
<input type="text" id="name" name="name" placeholder="请输入内容" />
</div>
<div class="form-item">
<label for="gender">性别:</label>
<select id="gender" name="gender">
<option value="男">男</option>
<option value="女">女</option>
</select>
</div>
<div class="form-item">
<label for="age">年龄:</label>
<input type="text" id="age" name="age" placeholder="请输入内容" />
</div>
<div class="form-item">
<label for="hobby">兴趣:</label>
<select id="hobby" name="hobby" multiple>
<option value="篮球">篮球</option>
<option value="足球">足球</option>
<option value="网球">网球</option>
<option value="排球">排球</option>
<option value="羽毛球">羽毛球</option>
</select>
</div>
<div class="form-item">
<label for="description">描述:</label>
<input
type="text"
id="description"
name="description"
placeholder="请输入内容"
/>
</div>
<div class="form-search">
<button type="submit">搜索</button>
<button type="reset">重置</button>
</div>
</form>
<div>
<table collapse="collapse">
<thead>
<th>编码</th>
<th>姓名</th>
<th>性别</th>
<th>年龄</th>
<th>兴趣</th>
<th>描述</th>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
筛选条件的取值/处理,以及筛选后数据的渲染也都是使用原生Javascript实现,所以没有使用任何第三方库。
const mockData = [
{
id: 1,
name: "张三",
gender: "男",
age: 18,
hobby: ["篮球", "网球"],
description: "我是张三",
},
{
id: 2,
name: "李四",
gender: "男",
age: 19,
hobby: ["排球", "足球"],
description: "我是李四",
},
{
id: 3,
name: "王五",
gender: "男",
age: 20,
hobby: ["网球", "羽毛球"],
description: "我是王五",
},
{
id: 4,
name: "赵六",
gender: "女",
age: 21,
hobby: ["羽毛球"],
description: "我是赵六",
},
{
id: 5,
name: "孙七",
gender: "女",
age: 22,
hobby: ["篮球", "网球", "羽毛球"],
description: "我是孙七",
},
];
const search = (e) => {
e.preventDefault();
var form = e.target;
const id = form.elements["id"];
const name = form.elements["name"];
const gender = form.elements["gender"];
const age = form.elements["age"];
const hobby = form.elements["hobby"];
const description = form.elements["description"];
let selectedHobby = [];
for (var i = 0; i < hobby.selectedOptions.length; i++) {
selectedHobby.push(hobby.selectedOptions[i].value);
}
// 清空选中的值,以便下次使用
hobby.selectedIndex = -1;
console.log({
id: id.value,
name: name.value,
gender: gender.value,
age: age.value,
hobby: selectedHobby,
description: description.value,
});
let conditions = [];
if (id.value) {
conditions.push({
type: "include",
key: 'id',
value: id.value,
})
}
if (name.value) {
conditions.push({
type: "include",
key: 'name',
value: name.value,
})
}
if (gender.value) {
conditions.push({
type: "equal",
key: 'gender',
value: gender.value,
})
}
if (age.value) {
conditions.push({
type: "include",
key: 'age',
value: age.value,
})
}
if (description.value) {
conditions.push({
type: "include",
key: 'description',
value: description.value,
})
}
const intersectionResult = intersectionFun(mockData, conditions);
let conditions2 = [];
if (selectedHobby.length > 0) {
selectedHobby.forEach(item => {
conditions2.push({
type: "include",
key: 'hobby',
value: item,
})
});
}
const result = unionFun(intersectionResult, conditions2);
rendTbody(result.sort((a, b) => a.id - b.id));
return false;
};
const rendTbody = (data) => {
const tbody = document.querySelector("#tableBody");
if (data.length === 0) {
tbody.innerHTML = 'No Data';
return;
}
tbody.innerHTML = '';
// 循环生成表格行和单元格
for (let i = 0; i < data.length; i++) {
const row = document.createElement("tr");
const person = data[i];
Object.keys(person).map((key) => {
const td = document.createElement("td");
td.textContent = `${person[key]}`;
td.style.border = "1px solid black";
row.appendChild(td);
});
tbody.appendChild(row);
}
};
const resetForm = () => {
rendTbody(mockData);
};
window.onload = () => {
rendTbody(mockData);
};
AI优化代码
写完找AI帮忙优化了一下,确实帮助发现了一些问题,也学到了不少东西。
筛选条件判断函数
请优化下面的函数:
function handleConditions(type = "include", leftVal, rightVal) {
if (type === "equal") {
return leftVal === rightVal;
}
if (type === "not-equal") {
return leftVal !== rightVal;
}
if (type === "include") {
return `${leftVal}`.indexOf(rightVal) !== -1;
}
if (type === "not-include") {
return `${leftVal}`.indexOf(rightVal) === -1;
}
}
代码检查与优化
检查问题:
-
类型安全:代码没有检查
leftVal和rightVal的类型,这可能导致类型不匹配时的错误比较。 -
性能:
handleConditions函数中对leftVal使用了字符串模板(${leftVal}),这意味着无论leftVal的原始类型是什么,它都会被转换为字符串。这可能导致不必要的类型转换和性能开销。 -
代码风格:虽然这种
if-else结构是有效的,但使用switch-case可能会使代码更简洁和易读。
优化后的代码:
function handleConditions(type = "include", leftVal, rightVal) {
switch (type) {
case "equal":
return leftVal === rightVal;
case "not-equal":
return leftVal !== rightVal;
case "include":
// 此处不采纳,因为数字类型也需要被筛选
// 只对字符串进行include检查,其他类型返回false
// return typeof leftVal === 'string' && leftVal.includes(rightVal);
return `${leftVal}`.includes(rightVal);
case "not-include":
// 此处不采纳,因为数字类型也需要被筛选
// 只对字符串进行not-include检查,其他类型返回true
// return typeof leftVal === 'string' && leftVal.includes(rightVal);
return !`${leftVal}`.includes(rightVal);
default:
return false; // 未知类型,默认返回false
}
}
优化点:
-
类型安全:在
include和not-include条件下,增加了对leftVal类型的检查,确保只对字符串执行includes操作。 -
性能:避免了不必要的字符串转换,只在字符串上执行
includes操作。 -
代码风格:使用
switch-case结构提高了代码的可读性。
这个优化版本的handleConditions函数在处理类型不匹配时更加健壮,并且针对字符串操作进行了优化,提高了性能。对于非字符串类型,include和not-include条件将分别默认返回false和true。如果需要进一步处理其他类型的include和not-include逻辑,可以在函数内部添加相应的代码。
这个优化后代码做了字符串类型的判断,但是这样的话数字类型就不会被筛选了,所以不能采纳。
数据交集函数
请优化下面的函数:
function intersectionFun(data, conditions) {
let tmp = [];
if (conditions.length === 0) {
tmp = data;
} else {
for (let index = 0; index < conditions.length; index++) {
const { type, key, value } = conditions[index];
if (index === 0) {
tmp = data.filter((item) => {
return handleConditions(type, item[key], value);
});
} else {
tmp = tmp.filter((item) => {
return handleConditions(type, item[key], value);
});
}
}
}
return tmp;
}
问题分析
代码片段中intersectionFun函数的主要逻辑是遍历conditions数组,并使用每个条件对data数组进行过滤。在每次迭代中,它都会更新tmp数组以包含符合当前条件的元素。
从逻辑上看,代码没有明显的错误,但是有一些地方可以进行优化以提高代码的清晰度和效率。
-
冗余的初始化:在函数开始时,
tmp被初始化为一个空数组。然后在if (conditions.length === 0)条件下,tmp被赋值为data。这个初始化步骤是多余的,因为当conditions为空数组时,函数将直接返回data,而不会使用tmp。 -
不必要的判断:
if (index === 0)条件用于区分第一次迭代和其他迭代。这实际上是不必要的,因为无论在哪次迭代中,tmp都使用filter方法进行更新。因此,这个判断可以移除。 -
可读性:将
conditions[index]解构为{ type, key, value }是一个好的做法,但为了提高可读性,可以考虑将其放在循环体内部,这样可以避免在循环的每次迭代中重复进行解构操作。
优化后的代码
function intersectionFun(data, conditions) {
if (conditions.length === 0) {
return data; // 直接返回data,不需要额外的tmp数组
}
let tmp = data.filter((item) => {
return handleConditions(conditions[0].type, item[conditions[0].key], conditions[0].value);
});
for (let index = 1; index < conditions.length; index++) {
const { type, key, value } = conditions[index]; // 将解构操作放在循环体内
tmp = tmp.filter((item) => {
return handleConditions(type, item[key], value);
});
}
return tmp;
}
优化点总结
-
移除了不必要的
tmp初始化。 -
删除了
if (index === 0)条件判断,简化了循环逻辑。 -
将解构操作移入循环体内,提高了代码的可读性。
数据并集函数
请优化下面的函数:
function unionFun(data, conditions) {
let tmp = [];
if (conditions.length === 0) {
tmp = data;
} else {
for (let index = 0; index < conditions.length; index++) {
const { type, key, value } = conditions[index];
tmp = tmp.concat(
data.filter((item) => {
return handleConditions(type, item[key], value);
})
);
}
}
return [...new Set(tmp)];
}
代码问题与优化点
-
重复过滤:在
unionFun函数中,每次循环都通过data.filter来过滤数据,这会导致多次遍历data数组,影响性能。 -
去重方式:使用
Set来去除重复项是正确的,但转换为Set和再转回数组可能会降低性能。 -
代码风格:
let tmp = [];和tmp = tmp.concat(...)的写法不够简洁。 -
逻辑优化:如果
conditions数组为空,则直接返回data,这一点做得很好。
修复与优化后的代码
function unionFun(data, conditions) {
// 此处不能采纳,因为后面是concat拼接的,初始需要为空数组
// let result = [...data]; // 初始结果集为data的副本
let result = [];
if (conditions.length === 0) {
result = data;
}
for (const { type, key, value } of conditions) {
// 使用filter一次性过滤满足条件的项,并扩展到结果集中
result = result.concat(data.filter(item => handleConditions(type, item[key], value)));
}
// 使用Set去重,并转回数组
return [...new Set(result)];
}
优化点总结
-
减少重复过滤:通过只遍历
data一次并累积结果,避免了重复过滤。 -
简化代码风格:使用扩展运算符
...来简化数组操作。 -
性能优化:虽然去重部分仍然使用了
Set,但在其他部分进行了优化,使得整体性能更佳。
请注意,虽然这里进行了一些性能优化,但在处理大数据集时,Set去重可能仍然是一个瓶颈。如果性能是首要考虑因素,可能需要考虑其他去重策略或数据结构。
AI 优化版 DEMO可以通过码上掘金体验多条件筛选-AI优化版 - 码上掘金 (juejin.cn)。
总结
- 前端也可以进行数据筛选,但是需要考虑到性能问题。
AI优化代码还是很强的,但也不能完全依赖AI,需要我们自己结合AI建议进行优化。