阅读 6672

当下拉列表数据过大时,该如何应对?

前言

后面有代码以及demo地址

在日常开发中,除了现成插件的使用外,还有很多问题是只能自己动手的。先抛出问题,当一个下拉列表的数据达到几千条甚至上万,这个时候浏览器已经会出现严重卡顿了。看看下面的例子

demo

如图所示,数据量达到2W简单测试数据(页面没有其他东西),点击加载下拉列表花了大概5s时间。出现这种情况心里真的是很复杂,这不是在玩我吗?

解决思路

这个问题其实和表格数据是同一个性能问题,表格的解决方式是通过分页器来减少页面承载的数据量。那么下拉列表该如何解决呢?通常我们都是一次性加载下拉的所有数据的,针对目前的难题,思路也是一样,采用分页来解决页面的性能问题。问题又来了,分页器是可以点击的,那下拉列表又不可以点击,那就只有在监听滚动事件里实现这件大事了。

先来大纲:

  1. 监听滚动
  2. 向下滚动时往后加载数据
  3. 向上滚动时往前加载数据
  4. 数据有进有出

好戏开始

1. 监听滚动

    <el-select class="remoteSelect" v-scroll v-model="value">
      <el-option :value="item.id" v-for="item in list" :key="item.id">{{item.name}}</el-option>
    </el-select>
复制代码

这里是基于vue与element-ui中el-select实现的监听滚动。这里是采用自定义指令的方式监听滚动

// directives目录下index.js文件

import Vue from 'vue'
export default () => {
  Vue.directive('scroll', {
    bind (el, binding) {
      // 获取滚动页面DOM
      let SCROLL_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap')
      SCROLL_DOM.addEventListener('scroll', function () {
        console.log('scrll')
      })
    }
  })
}
复制代码

在main.js中通过全局方法Vue.use()注册使用

import Directives from './directives'
Vue.use(Directives)
复制代码

这时滚动页面就可以看到控制的打印日志,代表监听已生效,接下来撸起袖子开干

2. 向下滚动时往后加载数据

首先要先判断出是向上滚动,还是向下滚动

  1. 记录上一次的滚动位置
  2. 当前位置与上一次的滚动位置作比较

通过一个公共变量来记录全局位置,通过scrollTop方法获取当前的滚动位置,并记录在公共变量scrollPosition

    bind (el, binding) {
      // 获取滚动页面DOM
      let SCROLL_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap')
      let scrollPosition = 0
      SCROLL_DOM.addEventListener('scroll', function () {
        // 当前的滚动位置 减去  上一次的滚动位置
        // 如果为true则代表向上滚动,false代表向下滚动
        let flagToDirection = this.scrollTop - scrollPosition > 0
        // 记录当前的滚动位置
        scrollPosition = this.scrollTop
        console.log(flagToDirection ? '滚动方向:下' : '滚动方向:上')
      })
    }
复制代码

目前已知晓滚动的方向,接下来便根据滚动方向做相应的处理。将滚动行为告诉组件

    ...省略
        // 记录当前的滚动位置
        scrollPosition = this.scrollTop
        // 将滚动行为告诉组件
        binding.value(flagToDirection)
复制代码

事件接受v-scroll指令中接受事件v-scroll="handleScroll",在该方法handleScroll处理滚动行为。 接下来只需要在该事件中针对为向下的滚动发起请求数据即可

    /*********************************
      ** Fn: handleScroll
      ** Intro: 处理滚动行为
      ** @params: param 为true代表向下滚动
      ** @params: param 为false代表向上滚动
    *********************************/
    handleScroll (param) {
      if (param) {
        // 请求下一页的数据
        this.list.push(...this.ajaxData(++this.pageIndex))
      }
    },
复制代码

到这里滚动加载已经实现。只是加载太频繁了,如果快速滚动则会同时发出多个请求后台数据,在密集一些游览器中ajax就要开发并发排队了,可见并不理想。那如何控制呢?那换种方式触发handleScroll事件,在滚动位置距离滚动页面底部一定高度时在触发,例如距页面底部只有100px时触发handleScroll事件

  1. scrollHeight获取滚动高度
  2. 在距底部100px时
        // 记录当前的滚动位置
        scrollPosition = this.scrollTop
        const LIMIT_BOTTOM = 100
        // 记录滚动位置距离底部的位置
        let scrollBottom = this.scrollHeight - (this.scrollTop + this.clientHeight) < LIMIT_BOTTOM
        // 如果已达到指定位置则触发
        if (scrollBottom) {
          // 将滚动行为告诉组件
          binding.value(flagToDirection)
        }
复制代码

通过数据长度的变化可以知道触发事件已经明显和谐了很多,这种效果很手机懒加载的方式一样,数据会被不断的叠加。

小提示: 会存在一个bug,即ajax是异步的,如果这个ajax请求花了1s才返回数据,而此时还在继续往下滚,那就会触发多个请求事件。如何避免这种情况呢? 答案是增加一个标志位,在请求前将该标志位设置为false,请求结束后设置为true。每次请求时先判断该标志位。如果为false则阻止该事件。

中场

再来看看我们的大纲

  1. 监听滚动
  2. 向下滚动时往后加载数据
  3. 向上滚动时往前加载数据
  4. 数据有进有出

到这里我们只完成①和②两个步骤。如果已经满足了你的需求,那你可以结束阅读了。如果对你有那么一点点帮助,先点个赞在离开。

前面说的都还只是基础操作,还没开始划重点呢。说好的无性能压力呢?

  1. 代码地址:github
  2. 当前版本demo:demo

先下班回家吃饭吧。周末继续写完 --2018-11-09 18:15


华丽的分割


就像周末一样,一切都会如期而至。--2018-11-10 08:30

3. 向上滚动时往前加载数据

handleScroll中判断参数param我们就得知了滚动行为,但之前我们只限制了向下滚动的触发时机,现在完善向上的滚动触发时机。同样的,先采用距离顶部100px的时候触发。

只要当前的滚动位置scrollTop小于100px就触发handleScroll事件

        // 如果向下滚动 并且距离底部只有100px
        if (flagToDirection && scrollBottom) {
          // 将滚动行为告诉组件
          binding.value(flagToDirection)
        }
        // 如果是向上滚动  并且距离顶部只有100px
        if (!flagToDirection && this.scrollTop < LIMIT_BOTTOM) {
          binding.value(flagToDirection)
        }
复制代码

handleScroll事件中我们就已经能检测到向上滚动行为了,并且触发时机也符合预期。

问题接踵而至,还一个比一个严重。一直向下滚动时分页加载则一直在累加,从第一页到一直滚动加载的页的数据都在列表里面了,那为何还需要向上加载呢?这里先埋下一个坑,先把4. 数据有进有出 看完这个坑就迎刃而解了。

4. 数据有进有出

说好的无性能压力呢?就在这个关键点了。看图一目了然(找这个效果图不容易呀):

  1. 向下滚动(图中每次点击即代表一次触发滚动加载数据)

2. 向上滚动

3. 有进有出

如上图效果,这就是我们最终要达成的目的。向上滚动我们就加载上一页的数据,向下滚动就加载下一页的滚动。数据实体list始终只有一定的数据量,数据量再大又能奈我何呢?

还是来看看如何实现吧

如何维持这个数组的长度呢?说起来有进有出很简单,但实现还是不简单的。

假设我们现在的数组容器最多容纳4页的数据量,每页100条数据。通过pageLimit参数来限定我们需要维护的数组长度,这里设为4。

当向下滚动或向上滚动时我们如何知道当前该加载那一页了?

所以我们需要一个记录表pageMap来记录页码,该页码表与当前的数据实体list对应。如下的对应关系。

pageLimit: 4
pageMap: [1, 2, 3, 4]
list:['第一页的数据', '第二页的数据', '第三页的数据', '第四页的数据']

复制代码

效果图(目前滚动不科学,步骤正确,后面有优化滚动):

    /*********************************
      ** Fn: handleScroll
      ** Intro: 处理滚动行为
      ** @params: param 为true代表向下滚动
      ** @params: param 为false代表向上滚动
    *********************************/
    handleScroll (param) {
      if (param) {
        if (this.pageMap.length >= this.pageLimit) {
          // 当长度相等的时候, 绝对不能超出长度  则有进必有出
          // 删除 pageMap 列表的第一个元素
          this.pageMap.shift()
          // 对应删除list中一页的数据量
          this.list.splice(0, this.pageSize)
        }
        ++this.pageIndex
        this.pageMap.push(this.pageIndex)
        // 请求下一页的数据
        this.list.push(...this.ajaxData(this.pageIndex))
        // 同步记录页码
      } else {
        // 如果在向上滚动时,如果还没有到达第一页则继续加载。 如果已到达则停止加载
        if (this.pageMap[0] > 1) {
          // 向上滚动,取出pageMap中第一个元素值减1
          this.pageIndex = this.pageMap[0] - 1
          // 同步设置分页
          // ①先删除最后一个元素
          this.pageMap.pop()
          // ②将新元素添加在头部
          this.pageMap = [this.pageIndex, ...this.pageMap]
          // ①删除list中最后一页的数据
          this.list.splice(-this.pageSize, this.pageSize)
          // ②将新数据添加在头部位置
          this.list = [...this.ajaxData(this.pageIndex), ...this.list]
        } else return false
      }
    }
复制代码

先写到这里吧,又该吃午饭了 2018-11.10 12:01

下午好,冬天的太阳暖洋洋的~ 2018-11-10 13:04

优化滚动

接下来咱们来填一下上面留下的坑,当数据达到指定长度时,数据总量不会变了,那么滚动的总体高度scrollHeight也就固定了,这是数据虽然有进有出,但是对滚动位置scrollTop将不再有影响,如上面的动态图中效果,将会一滚到底,而此时却还不是分页的终点,却让用户误以为到底了~~ 这个问题有点严重,有点严重~

优化方法:

  1. 每次加载数据后将当前滚动位置回到总体滚动高度的中间位置。 此时我们需要将滚动dom以及中间位置的高度通过自定义指令v-scroll抛出来,在往头部添加数据或尾部添加数据时滚动位置定位到中间位置。

抛出DOM和滚动的中间位置

// directives目录下index.js文件
        // 如果向下滚动 并且距离底部只有100px
        if (flagToDirection && scrollBottom) {
          // 将滚动行为告诉组件
          binding.value(flagToDirection, SCROLL_DOM, this.scrollHeight / 2)
        }
        // 如果是向上滚动  并且距离顶部只有100px
        if (!flagToDirection && this.scrollTop < LIMIT_BOTTOM) {
          binding.value(flagToDirection, SCROLL_DOM, this.scrollHeight / 2)
        }
复制代码

pageMap(对应list长度)达到pageLimit长度时,进出增删数据时重置DOM滚动位置

    /*********************************
      ** Fn: handleScroll
      ** Intro: 处理滚动行为
      ** @params: param 为true代表向下滚动 为false代表向上滚动
      ** @params: el 滚动DOM
      ** @params: middlePosition 滚动列表的中间位置
    *********************************/
    handleScroll (param, el, middlePosition) {
      if (param) {
        if (this.pageMap.length >= this.pageLimit) {
          ....省略代码
          this.list.splice(0, this.pageSize)
          // 回滚到中间位置
          el.scrollTop = middlePosition
        }
        ....省略代码
      } else {
        // 如果在向上滚动时,如果还没有到达第一页则继续加载。 如果已到达则停止加载
        if (this.pageMap[0] > 1) {
            ....省略代码
          this.list = [...this.ajaxData(this.pageIndex), ...this.list]
          // 回滚到中间位置
          el.scrollTop = middlePosition
        } else return false
      }
    },
复制代码

效果如上图所示,应该要结尾了?仔细观察的同学又发现彩蛋了。在滚动跳跃的一瞬间,原来用户看到的数据由于跳动后数据不在是原来用户看到的数据了,呀呀呀 ..... 这个问题有点严重,得慌

2. 优化滚动临界点 临界点即距离滚动总体高度顶部或底部一定距离时,触发handleScroll的临界点,即常量LIMIT_BOTTOM。之前定义的const LIMIT_BOTTOM = 100为100,这个是没啥道理,那么来个正经的临界点。

条件梳理

  1. 每次回到到 1/2 scrollHeight的位置
  2. 每次数据的变化位置为 (1 / pageLimit) * scrollHeight,这里演示的是1/4 * scrollHeight
  3. 设置一个未知数 X 为跳跃的临界点
  4. 临界点是用户在跳跃前看到的位置
  5. 1/2 scrollHeight是用户跳跃之后的位置

表达式: x + (1/4 * scrollHeight) = (1/2 scrollHeight)

x = (1/4 * scrollHeight),即 const LIMIT_BOTTOM = this.scrollHeight / 4 那我们再开看看滚动情况:

效果已经差不多了,如果想用户最后看到位置在靠下一些,可以设置const LIMIT_BOTTOM = this.scrollHeight / 4.2

结语

故事到这里终于结束了。点个赞 给个鼓励咯~

在github新建一个仓库来上传代码:

  1. demo查看:demo
  2. github代码查看:传送门
  3. 这篇很实用呀,则没人欣赏呢Vue自定义指令实现input限制输入正整数
  4. 为何要再封装 AJAX?
  • 版权说明:本文首发于掘金,如需转载请注明出处。

种一棵树最好的时间是十年前,其次是现在。 --谁说的不重要。

关注下面的标签,发现更多相似文章
评论