最近开发遇到一个比较复杂的数据处理,大概需求是有多个楼幢,楼幢下有多个单元,单元下有多个户室,可以单击选择户室,点击单元的多选框可以全选当前单元下的所有户室,点击楼幢的多选框可以全选当前楼幢下所有单元的户室,因此选中数组selectedIds中存放的是各个户室对象。数据模型是树形结构,由于样式要求,没有办法使用树状插件,只能自己来实现多选。效果图如下:
当写到某个楼幢的取消选择时发现了问题,因为去掉selectedIds中该单元下的所有户室,首先需要遍历所有楼幢,找到当前取消选择的楼幢,随后遍历该楼幢下的所有单元,然后再遍历各个单元下的户室,最后遍历selectedIds,找到其中相匹配的id,然后删除该匹配的户室对象。但是按照这样去写会出现两个问题:1、由于每次遍历时索引的变化,splice后下次遍历的索引可能就变成下下个了。2、由于是四重循环,一旦数据量很庞大,且处于最不理想情况下,数据的处理时间会很长,经过测试,一千七百多条数据处理了将近十几秒,这肯定不行的,需要优化。
解决问题1首先想到的是filter,将该楼幢下所有单元下的户室过滤掉,将其余的户室留在数组中,利用新数组接收过滤的值而不是删除,从而避免了索引变化产生的问题。
解决问题2的重点就在于优化循环遍历的次数,首先想到的就是break在完成目的之后跳出循环,但是由于解决问题1利用的filter,而filter、map、forEach这些是没法利用break来跳出循环的,虽然也考虑到可以使用some、every这些可以立即跳出循环的方法,但是需要加上更多的变量进行判断,决定还是自己利用for循环写个可以break的filter。代码如下:
//达成目标直接break的filter
myFilter(arr, fn) {
let newArr = [];
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
if (fn(item, i, arr)) {
newArr.push(arr[i]);
} else if (i !== arr.length - 1) {
let afterArr = arr.slice(i + 1);
newArr = newArr.concat(afterArr);
break;
}
}
return newArr;
},
changeCheckBox(value, current) {
if (this.loading) {
return;
}
let selectedIds = this.selectedIds;
this.loading = true;
this.rangeObj.lzs.map(lz => {
if (lz.lzdm === current.lzdm) {
lz.dys.map(dy => {
dy.checked = value;
dy.fhs.map(fh => {
if (value) {
selectedIds.push(fh);
} else {
//调用myFilter
selectedIds = this.myFilter(selectedIds, select => {
return select.fhdm !== fh.fhdm;
});
}
});
});
}
});
this.selectedIds = this.distinct(selectedIds, "fhdm");
this.loading = false;
},
使用时遍历到各个户室,然后根据value来进行判断,value为true时代表全选,由于selectedIds中包含多个楼幢的多个单元多个户室对象,所以如果再次判断是否已经在selectedIds中情况会非常复杂,这里选择直接push进去,并在最后执行distinct方法进行数组中对象的去重。value为false代表取消全选,则调用写好的myFilter方法,第一个参数arr为待遍历数组,第二个参数是一个回调方法,由myFilter函数传递当前对象item、当前索引index、当前数组arr,并且根据返回值是true,即在selectedIds中找不到与之相等的fhdm(id),则push到这个新数组中。后面的else if是为这里的需求单独加的一段代码,即如果返回值是false,且并非是最后一项,则代表后面的对象都不可能再有相等的fhdm(id)了(各单元下各户室在selectedIds中都是相邻的),直接将后面的对象concat到新数组中,并且break,最后return返回这个新数组,节省了大量无意义遍历时间。优化过后的代码执行效率比之前快了不止一个档次。同样的,后面的单元的取消全选也可以复用这个myFilter方法:
changeCheckBoxDy(value, current, lzObj) {
if (this.loading) {
return;
}
this.rangeObj.lzs[this.currentIndex].checked = lzObj.dys.every(dyItem => {
return dyItem.checked;
});
let selectedIds = this.selectedIds;
this.loading = true;
this.rangeObj.lzs[this.currentIndex].dys.map(dy => {
if (
(typeof dy.dydm === "undefined" &&
typeof current.dydm === "undefined") ||
dy.dydm === current.dydm
) {
dy.fhs.map(fh => {
if (value) {
selectedIds.push(fh);
} else {
selectedIds = this.myFilter(selectedIds, select => {
return select.fhdm !== fh.fhdm;
});
}
});
}
});
this.selectedIds = this.distinct(selectedIds, "fhdm");
this.loading = false;
},
顺便也附上数组中对象去重方法(arr为待去重数组,key为标识符,即根据哪个键来去重):
distinct(arr, key) {
var newobj = {},
newArr = [];
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
if (!newobj[item[key]]) {
newobj[item[key]] = newArr.push(item);
}
}
return newArr;
},
当然这里其实还可以继续优化,比如在点击多选框时,直接传当前楼幢下的单元数组,可以减少一次所有楼幢的遍历;各个map因为也是遍历所有次,也可以利用for循环重写map、forEach,并且在找到对象后break,都可以进一步优化代码。
各楼幢、单元的全选以及取消全选就完成了,最后还剩下单个户室的选中以及取消选中,由于需要遍历selectedIds选中数组,当找到当前选中/取消选中的对象后应当break,因此选择利用for循环写个可以break的forEach,代码如下:
//达成目标直接break的forEach
myForEach(arr, fn) {
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
if (fn(item, i, arr)) {
break;
}
}
},
selectFh(obj) {
if (this.isEdit && !this.isDetail) {
let selectedIds = this.selectedIds;
let currentIndex = -1;
this.myForEach(selectedIds, (item, index) => {
if (item.fhdm === obj.fhdm) {
currentIndex = index;
return true;
}
});
if (currentIndex !== -1) {
selectedIds.splice(currentIndex, 1);
} else {
selectedIds.push(obj);
}
}
},
定义一个currentIndex变量,默认值为-1代表未找到,随后调用myForEach方法,同样第一个参数arr为待遍历数组,第二个参数fn为回调函数,由函数内部传递当前对象item、当前索引index、当前数组arr供外部使用,若返回值为true则break跳出循环。最后根据currentIndex的值来决定是新增还是删除。
至于各户室是否显示选中颜色则根据动态class来判断,代码如下:
<div class="lpb-dy-content-item-fh-box">
<template v-for="(items, idx) in item.fhs">
<span
:class="judgeClass(items)"
:title="'建筑面积:' + items.jzmj + 'm²'"
:key="idx"
@click="selectFh(items)">
{{ items.sh ? items.sh : "——" }}
</span>
</template>
</div>
judgeClass(items) {
let flag = this.selectedIds.some(it => {
return it.fhdm === items.fhdm;
});
return [
"lpb-fh",
{
"lpb-fh-select": flag
}
];
},
这样各功能都已经实现了,且代码执行效率也提升了很多,剩下的就是一些细节性的东西就不多加赘述啦~。