一、业务场景

在使用element-ui展示表格数据时,需要根据查询条件对数据进行搜索,此时需使用el-select,并且需要从服务端获取数据用el-option展示给用户选择,但是由于搜索条件使用的是业务的数据,所以数据量比较大,服务端返回的数据量达到了5000多条,还需要在下拉框中展示。

此时会出现两个问题:
1、直接影响:渲染会导致页面卡顿且需要很长时间等待,用户体验极差;
2、间接影响:展示出来后,用户进行选择后查询,这里没问题,但是如果想去关闭选项卡的时候发现,关闭得极慢,这也是由于el-option数据量过大导致,因为渲染出来5000多个dom节点在页面上,vue需要去销毁的时间也就更长,反应就更慢了;

二、相关知识

(一)Vue实现自定义指令(directive)

(1)自定义指令(directive)使用场景

Vue除了核心功能默认内置的指令 (v-modelv-show),Vue也允许注册自定义指令。在你需要对普通DOM元素进行底层操作的情况下,这时候就会用到自定义指令directive。Vue自定义指令有全局注册和局部注册两种方式。先来看看注册全局指令的方式,通过Vue.directive(id, [definition])方式注册全局指令。然后在入口文件中进行 Vue.use()调用。
批量注册指令,新建directives/index.js文件

(2)注册方式以及用法

directive分为全局注册以及局部注册两种,注册后在标签上使用v-注册名即可使用,在下例中,使用v-name

1
2
3
4
5
6
7
8
9
10
11
12
// 全局注册
Vue.directive('name',function(el, binding, vNode){
  // do something
});
// 局部注册
new Vue({
  directives:{
    name:{
      // do something
    }
  }
});

(3)钩子函数

在注册directive时,一个指令定义对象可以提供如下几个钩子函数 (均为可选):

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 。

componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind: 只调用一次,指令与元素解绑时调用。

我们在钩子函数中可以获取到以下参数:

el :指令所绑定的元素,可以用来直接操作 DOM

binding:一个对象,包含以下 property:
name:指令名,不包括v-前缀
value:指令的绑定值,例如:v-my-directive="1 + 1"中,绑定值为2
oldValue:指令绑定的前一个值,仅在updatecomponentUpdated钩子中可用。无论值是否改变都可用
expression:字符串形式的指令表达式。例如v-my-directive="1 + 1"中,表达式为"1 + 1"
arg:传给指令的参数,可选。例如v-my-directive:foo中,参数为"foo"
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar中,修饰符对象为{ foo: true, bar: true }

vnode:Vue 编译生成的虚拟节点

oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用

注意!除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

举个例子,我们在项目中的按钮需要通过权限来控制是否渲染,那么我们可以做一个权限指令来判断是否渲染,

html:

1
2
3
4
5
6
7
<template>
<div>
<button v-permission="['add_btn']">新增</button>
<button v-permission="['edit_btn']">编辑</button>
<button v-permission="['remove_btn']">删除</button>
</div>
</template>

javascript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 模拟从后端拿到权限数据为一个数组,里面有新增以及编辑两个按钮权限:
const data = [ 'add_btn', 'edit_btn' ]

export default {
name: '',
directives: {
// 定义一个 permission 的指令
permission: {
// 在节点被插入父节点时调用
inserted (el, binding, vnode) {
const { value } = binding
const permissionRoles = value

const hasPermission = data.some(role => {
return permissionRoles.includes(role)
})
// 如果没有权限,则移除按钮节点
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
}
}

这样,就完成了一个按钮级别的权限管理,自定义指令当然还有更多的用处,比如按钮的点击波纹,按钮点击请求返回前的禁用,某节点加载时loading,如开头所说的,在你需要对普通DOM元素进行底层操作的情况下,这时候就会用到自定义指令directive

(二)Vue中实现函数的防抖、节流

(1)介绍

1、**防抖(debounce)**:触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间。举例:就好像在百度搜索时,每次输入之后都有联想词弹出,这个控制联想词的方法就不可能是输入框内容一改变就触发的,他一定是当你结束输入一段时间之后才会触发。
**节流(thorttle)**:高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。举例:预定一个函数只有在大于等于执行周期时才执行,周期内调用不执行。就好像你在淘宝抢购某一件限量热卖商品时,你不断点刷新点购买,可是总有一段时间你点上是没有效果,这里就用到了节流,就是怕点的太快导致系统出现bug。

2、区别:防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

(2)使用场景

[1] DOM 元素的拖拽功能实现(mousemove)
[2] 搜索联想(keyup)
[3] 计算鼠标移动的距离(mousemove)
[4] Canvas 模拟画板功能(mousemove)
[5] 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
[6] 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次.

(3)函数的防抖

函数的防抖是在多少时间后再来执行函数,我们可以理解为这样的一种生活场景(坐升降电梯),在点击电梯的开门按钮后,电梯会开门,然后等待一段时间来关门。但是如果在等待的期间,有人再次点击开门按钮,那么电梯后继续等待关门时间,直到等待关门时间结束,没有人来点击开门按钮后,电梯才会开始工作。

第一次非立即执行

1
2
3
4
5
6
7
8
9
export function _debounce(f, t){
let timer;
return (...arg) => {
clearTimeout(timer);
timer = setTimeout(() =>{
f( ...arg)
}, t)
}
}

对于有些场景来说,第一次我不需要等待,需要立即执行,例如:打开控制台获取窗口试图大小(这里我们需要一直改变窗口的大小,等停下来再次获取窗口视图大小)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function _debounceFirstExe(f, t){
let timer, flag = true;
return (...args) => {
if (flag){
f(...args);
flag = false;
}else {
clearTimeout(timer);
timer = setTimeout(() => {
f(...args);
flag = true;
}, t)
}
}
}

最终合并的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export  function  _debounce(f, t,im = false){
let timer, flag = true;
return (...args) => {
// 需要立即执行的情况
if (im){
if (flag){
f(...args);
flag = false;
}else {
clearTimeout(timer);
timer = setTimeout(() => {
f(...args);
flag = true
}, t)
}
}else {
// 非立即执行的情况
clearTimeout(timer);
timer = setTimeout(() => {
f(...args)
}, t)
}
}
}

函数防抖对于我们代码层面我们可以用在哪里呢?
在点赞、输入框校验、取消点赞、创建订单等发送网络氢气的时候,如果我们连续点击按钮,可能会发送多次请求。这个对于后台来说是不允许的。在鼠标每次resize/scroll触发统计事件.

(4)函数节流

与函数防抖的胞兄,函数节流的原理也是大同小异,函数节流是在一定时间内我只会执行一次。

第一次非立即执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function _throttle(f,t){
let timer=true;
return (...arg)=>{
if(!timer){
return;
}
timer=false;
setTimeout(()=>{
f(...arg);
timer=true;
},t)

}
}

在效果中,我们点击了非常多次,但是就只执行了4次,因为我规定的时间是1000ms执行一次。这样也是减少了执行次数。

第一次立即执行版本

1
2
3
4
5
6
7
8
9
10
11
12
export function _throttleFirstExt(f, t) {
let flag = true;
return (...args) => {
if (flag) {
f(...args);
flag = false;
setTimeout(() => {
flag = true
}, t)
}
}
}

这里我们看到了,第一次点击会立马执行。

最终合并后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
export function _throttle(f, t, im = false){
let flag = true;
return (...args)=>{
if(flag){
flag = false
im && f(...args)
setTimeout(() => {
!im && f(...args)
flag = true
},t)
}
}
}

三、解决问题

由于5000多条数据用户不可能一直滚动下去找自己想要数据,记得设置filterable属性,实现搜索功能.

如果仅设置element-uifilterable属性,那么搜索的范围只有懒加载已滚动出的数据,导致搜索不全、不准确。为了解决这个问题,我们又继续使用了filter-method属性并结合visible-change事件,以及搜索输入时增加防抖进行优化。

最终代码:

在main.js中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.directive('el-select-loadmore', {
inserted: (el, binding) => {
// 获取element-ui定义好的scroll盒子
const SELECTWRAP_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap')
if (SELECTWRAP_DOM) {
SELECTWRAP_DOM.addEventListener('scroll', function() {
/**
* scrollHeight 获取元素内容高度(只读)
* scrollTop 获取或者设置元素的偏移值,
* 常用于:计算滚动条的位置, 当一个元素的容器没有产生垂直方向的滚动条, 那它的scrollTop的值默认为0.
* clientHeight 读取元素的可见高度(只读)
* 如果元素滚动到底, 下面等式返回true, 没有则返回false:
* ele.scrollHeight - ele.scrollTop === ele.clientHeight;
*/
const condition = this.scrollHeight - this.scrollTop <= this.clientHeight
if (condition) binding.value()
})
}
}
})

在utils/index.js下面添加_debounce方法

1
2
3
4
5
6
7
8
9
10
11
12
// 防抖流
export function _debounce(fn, delay = 300) {
let timer = null
return function() {
const _this = this
const args = arguments
if (timer) clearTimeout(timer)
timer = setTimeout(function() {
fn.apply(_this, args)
}, delay)
}
}

业务模块的页面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<el-form-item label="物料:">
<el-select v-model="form.materialcode"
v-el-select-loadmore="loadMore(maxNumber)"
clearable
filterable
:filter-method="filterMethod"
placeholder="请选择物料"
@visible-change="visibleChange">
<el-option
v-for="(item, i) in materialList.slice(0, maxNumber)"
:key="i"
:label="item.materialcode"
:value="item.materialsid" />
</el-select>
</el-form-item>
<script>
import { _debounce } from '@/utils/index'
import { MaterialList } from '@/api/common'
export default {
name: 'Stockt',
data() {
return {
form: {
materialsid: ''
},
maxNumber: 20,
materialList: [],
resMaterialList: [],
}
},
mounted() {
MaterialList({ materialcode: '', materialType: 'MC' }).then(res => {
this.resMaterialList = res.data
})
this.handleSearch()
},
methods: {
loadMore() { // 每次滚动到底部可以新增条数
return () => (this.maxNumber += 10)
},
filterMethod: _debounce(function(filterVal) { // 筛选方法
if (filterVal) {
this.$set(this.form, 'materialcode', filterVal) // 注意:这里需要重新赋值一遍value,否则会出现清空的现象
const filterArr = this.resMaterialList.filter((item) => {
return item.materialcode.toLowerCase().includes(filterVal.toLowerCase())
})
this.materialList = filterArr
} else {
this.materialList = this.resMaterialList
}
}, 500),
visibleChange(flag) { // 下拉框出现时,调用过滤方法
if (flag) {
this.filterMethod()
}
}
}
}
</script>