阅读 58

透过背后源码学,拿来就用的el-pagination分页组件

前言

在平时使用element ui 的时候,通过后台返回数据进行列表渲染的操作非常多。每次用到的组件无非是 el-table相关的组件。当数据渲染时,还有一个很重要的操作就是分页,当然element ui提供了el-pagination组件去实现这个功能。

el-pagination官网效果 (完整功能)

日常使用

首先来看一下平时时如何使用的:

<el-pagination
   class="pagination"
   background
   layout="total, prev, pager, next, sizes"
   :page-sizes="[15, 20, 30]"
   :total="total"
   :current-page.sync="current_page"
    @size-change="eventSizeChange"
    @current-change="eventCurrentChange"
></el-pagination>
复制代码
data () {
    total:0,
    current_page:1,
    params: {
       page:1,
       page_size:20
    }
}

methods () {
//假设是一个请求接口的函数
getList() {
 getList(this.params) {
//处理数据
}
}

// 分页
eventCurrentChange (page) {
   this.params.page = page
   this.getList()
 },
eventSizeChange (size) {
   this.params.page_size = size
   this.params.page = 1
   this.current_page = 1
   this.getList()
}
}
复制代码

当你把这个两个分页参数传给后端的时候,后端会进行处理,把需要的数据传回给你。

而项上面官网所展示的分页组件,element ui对其封装又是如何思考的呢?

下面我们一起来看看吧

学习目标时能够理解文章末尾的一些知识点

源码目录

首先理清两个文件的大体内容: 我就直接用思维导图先描述一下:

文件一、pangination.js

就是对整个组件的封装

包括总页数,每页条数,按钮跳转,输入跳转等等

先分析如何将el-pagination封装 src/pangination.js

1、Props

在pangination.js中,作为props使用

props: {
    //每页显示条目个数,支持 .sync 修饰符
    pageSize: {
      type: Number,
      default: 10
    },
    //是否使用小型分页样式
    small: Boolean,
    // 总条目数
    total: Number,
    //总页数,total 和 page-count 设置任意一个就可以达到显示页码的功能;如果要支持 page-sizes 的更改,则需要使用 total 属性
    pageCount: Number,
    //页码按钮的数量,当总页数超过该值时会折叠
    pagerCount: {
      type: Number,
      validator(value) {
        return (value | 0) === value && value > 4 && value < 22 && (value % 2) === 1;
      },
      default: 7
    },
    // 当前页数,支持 .sync 修饰符
    currentPage: {
      type: Number,
      default: 1
    },
    //组件布局,子组件名用逗号分隔
    layout: {
      default: 'prev, pager, next, jumper, ->, total'
    },
    //每页显示个数选择器的选项设置
    pageSizes: {
      type: Array,
      default() {
        return [10, 20, 30, 40, 50, 100];
      }
    },

    popperClass: String,

    prevText: String,

    nextText: String,

    background: Boolean,

    disabled: Boolean,
    //只有一页时是否隐藏
    hideOnSinglePage: Boolean
  },
复制代码

2、render

当然这里面涉及了很多组件prev, pager, next, jumper, ->, total,我们等下再说

 render(h) {
    //this.layout就是之前我们再props里面定义的组件布局,如果每页子组件那么就显示null
    const layout = this.layout;
    if (!layout) return null;
    //处理只有一页是否隐藏
    if (this.hideOnSinglePage && (!this.internalPageCount || this.internalPageCount === 1)) return null;

    // template 是一个父容器模板,定义的时候可以先处理它的样式,包括背景以及小样式设定
    let template = <div class={['el-pagination', {
      'is-background': this.background,
      'el-pagination--small': this.small
    }] }></div>;

    //TEMPLATE_MAP 是一个组件集合,里面涉及的组件会在后面定义
    const TEMPLATE_MAP = {
      prev: <prev></prev>, // 上一页
      jumper: <jumper></jumper>, // 跳转 前往多少页
      pager: <pager currentPage={ this.internalCurrentPage } pageCount={ this.internalPageCount } pagerCount={ this.pagerCount } on-change={ this.handleCurrentChange } disabled={ this.disabled }></pager>,
      next: <next></next>, // 下一页
      sizes: <sizes pageSizes={ this.pageSizes }></sizes>, // 每页显示条目个数
      slot: <slot>{ this.$slots.default ? this.$slots.default : '' }</slot>,
      total: <total></total> // 总共的页数
    };

    //
    const components = layout.split(',').map((item) => item.trim());
    const rightWrapper = <div class="el-pagination__rightwrapper"></div>;
    let haveRightWrapper = false;

    template.children = template.children || [];
    rightWrapper.children = rightWrapper.children || [];
    //  // ->这个符号主要是将其后面的组件放在rightWrapper中,然后右浮动;如果存在->符号,就将haveRightWrapper为true
    components.forEach(compo => {
      if (compo === '->') {
        haveRightWrapper = true;
        return;
      }
      // 当haveRightWrapper为true,即在->后面的放入rightWrapper中
      if (!haveRightWrapper) {
        template.children.push(TEMPLATE_MAP[compo]);
      } else {
        rightWrapper.children.push(TEMPLATE_MAP[compo]);
      }
    });

    if (haveRightWrapper) {
      template.children.unshift(rightWrapper);
    }

    return template;
  },

复制代码

3、methods :

  • handleCurrentChange(val)
  • prev()
  • next()
  • getValidCurrentPage(value)
  • emitChange()

4、computed:

5、watch:

6、components 组件:

prev

上一页组件 就是点击'上一页'或者前进按钮 (以及边界处理)

 render(h) {
    return (
        <button
        type="button"
        class="btn-prev"
        disabled={ this.$parent.disabled || this.$parent.internalCurrentPage <= 1 }
        on-click={ this.$parent.prev }>
        {
            this.$parent.prevText
            ? <span>{ this.$parent.prevText }</span>
            : <i class="el-icon el-icon-arrow-left"></i>
        }
        </button>
    );
    }
复制代码

next

下一页操作,和prev类似

 render(h) {
    return (
        <button
        type="button"
        class="btn-next"
        disabled={ this.$parent.disabled || this.$parent.internalCurrentPage === this.$parent.internalPageCount || this.$parent.internalPageCount === 0 }
        on-click={ this.$parent.next }>
        {
            this.$parent.nextText
            ? <span>{ this.$parent.nextText }</span>
            : <i class="el-icon el-icon-arrow-right"></i>
        }
        </button>
    );
    }
复制代码

sizes

这个组件涉及的东西比较多,先总览一下

  1. mixins: [Locale],
  2. props
props: {
    pageSizes: Array
},
复制代码
  1. watch immediate:true,代表watch里面声明了之后会立马执行handler里面的函数。
 watch: {
    pageSizes: {
        immediate: true,
        handler(newVal, oldVal) {
        if (valueEquals(newVal, oldVal)) return;
        if (Array.isArray(newVal)) {
            this.$parent.internalPageSize = newVal.indexOf(this.$parent.pageSize) > -1
            ? this.$parent.pageSize
            : this.pageSizes[0];
        }
        }
    }
},
复制代码
  1. render

也就是需要渲染出这个样式:

render(h) {
    return (
        <span class="el-pagination__sizes">
        <el-select
            value={ this.$parent.internalPageSize }
            popperClass={ this.$parent.popperClass || '' }
            size="mini"
            on-input={ this.handleChange }
            disabled={ this.$parent.disabled }>
            {
            this.pageSizes.map(item =>
                <el-option
                value={ item }
                label={ item + this.t('el.pagination.pagesize') }>
                </el-option>
            )
            }
        </el-select>
        </span>
    );
},
复制代码
  1. components
 components: {
        ElSelect,
        ElOption
      },
复制代码
  1. methods
methods: {
    handleChange(val) {
        if (val !== this.$parent.internalPageSize) {
        this.$parent.internalPageSize = val = parseInt(val, 10);
        this.$parent.userChangePageSize = true;
        this.$parent.$emit('update:pageSize', val);
        this.$parent.$emit('size-change', val);
        }
    }
}
复制代码

jumper

mixins: [Locale],

      components: { ElInput },

      data() {
        return {
          userInput: null
        };
      },

      watch: {
        '$parent.internalCurrentPage'() {
          this.userInput = null;
        }
      },

      methods: {
        handleKeyup({ keyCode, target }) {
          // Chrome, Safari, Firefox triggers change event on Enter
          // Hack for IE: https://github.com/ElemeFE/element/issues/11710
          // Drop this method when we no longer supports IE
          if (keyCode === 13) {
            this.handleChange(target.value);
          }
        },
        handleInput(value) {
          this.userInput = value;
        },
        handleChange(value) {
          this.$parent.internalCurrentPage = this.$parent.getValidCurrentPage(value);
          this.$parent.emitChange();
          this.userInput = null;
        }
      },

      render(h) {
        return (
          <span class="el-pagination__jump">
            { this.t('el.pagination.goto') }
            <el-input
              class="el-pagination__editor is-in-pagination"
              min={ 1 }
              max={ this.$parent.internalPageCount }
              value={ this.userInput !== null ? this.userInput : this.$parent.internalCurrentPage }
              type="number"
              disabled={ this.$parent.disabled }
              nativeOnKeyup={ this.handleKeyup }
              onInput={ this.handleInput }
              onChange={ this.handleChange }/>
            { this.t('el.pagination.pageClassifier') }
          </span>
        );
      }
    },
复制代码

total

 mixins: [Locale],

      render(h) {
        return (
          typeof this.$parent.total === 'number'
            ? <span class="el-pagination__total">{ this.t('el.pagination.total', { total: this.$parent.total }) }</span>
            : ''
        );
      }
    },

    Pager // 单独分出来
复制代码

文件二、 pager.vue

<template>
  <ul @click="onPagerClick" class="el-pager">
    <li
      :class="{ active: currentPage === 1, disabled }"
      v-if="pageCount > 0"
      class="number">1</li>
      <!-- 显示第一页 -->
      <!-- 如果当前页数为1,那么样式为显蓝色,且按钮为禁用 -->
      <!-- 总页数大于0的时候才会显示哦 -->
    <li
      class="el-icon more btn-quickprev"
      :class="[quickprevIconClass, { disabled }]"
      v-if="showPrevMore"
      @mouseenter="onMouseenter('left')"
      @mouseleave="quickprevIconClass = 'el-icon-more'">
      <!-- 显示向右更多 没有移上去就是...,移上去就是一个向右的图标 -->
      <!-- quickprevIconClass 往前的图标 -->
      <!-- showPrevMore 这个为真的时候才显示,因为有时候页数没达到那么多,...就不会显示,因此下面需要判断 -->
      <!-- 两个移入移除的操作就是样式改变了 -->
    </li>
    <li
      v-for="pager in pagers"
      :key="pager"
      :class="{ active: currentPage === pager, disabled }"
      class="number">{{ pager }}</li>
      <!-- 控制要显示哪些页数 -->
      <!-- pager是一个计算属性 -->
    <li
      class="el-icon more btn-quicknext"
      :class="[quicknextIconClass, { disabled }]"
      v-if="showNextMore"
      @mouseenter="onMouseenter('right')"
      @mouseleave="quicknextIconClass = 'el-icon-more'">
      <!-- 显示向右更多 没有移上去就是...,移上去就是一个向右的图标 -->
    </li>
    <li
      :class="{ active: currentPage === pageCount, disabled }"
      class="number"
      v-if="pageCount > 1">{{ pageCount }}</li>
      <!-- 显示总页数 -->
  </ul>
</template>
复制代码

通过上面简单的说明,下面我们一起来看看需要了解的几个方法:

1、onPagerClick() 这个方法被绑定在ul上,而不是li标签上,使用到的是事件代理模式

DOM操作是十分消耗性能的,所以重复的事件绑定简直是性能杀手。而事件代理的核心思想,就是通过尽量少的绑定,去监听尽量多的事件。虽然这里只有五个li标签,但是通过事件代理,也能减少一定的性能消耗

onPagerClick(event) {
    const target = event.target;
    if (target.tagName === 'UL' || this.disabled) {
        return;
    }

    let newPage = Number(event.target.textContent);
    const pageCount = this.pageCount;//总页数
    const currentPage = this.currentPage; // 当前页数
    const pagerCountOffset = this.pagerCount - 2; // 出去前后按钮剩下的

    if (target.className.indexOf('more') !== -1) {
        if (target.className.indexOf('quickprev') !== -1) {
        newPage = currentPage - pagerCountOffset;
        } else if (target.className.indexOf('quicknext') !== -1) {
        newPage = currentPage + pagerCountOffset;
        }
    }

        /* istanbul ignore if */
    if (!isNaN(newPage)) {
        if (newPage < 1) {
        newPage = 1;
        }

        if (newPage > pageCount) {
        newPage = pageCount;
        }
    }

    if (newPage !== currentPage) {
        this.$emit('change', newPage);
    }
},
复制代码

2、showPrevMore showNextMore

由四个图可以看出,more按钮出现的情况有四种

  • 没有more
  • 左右都有more
  • 左边有,右边没有
  • 右边有,左边没有

那么又是怎么实现的呢?

因此这边需要处理两个问题,一个是监听watch,当存在有more的情况,要显示...的图标样式(下面都简称为more)。一个是计算computed,计算是否需要more,该如何用。

  • 先来看watch吧,简单一些:
watch: {
      showPrevMore(val) {
        if (!val) this.quickprevIconClass = 'el-icon-more';
      },

      showNextMore(val) {
        if (!val) this.quicknextIconClass = 'el-icon-more';
      }
    },
复制代码
  • computed

了解清楚变量的意思,下面这段代码就是实现刚才的四种情况 pagerCount 就是要显示的按钮数量 halfPagerCount 一半 currentPage 当前页数 pageCount 总页数

下面我就拿一个左右都有的例子来理一下这个思路:

 pagers() {
    const pagerCount = this.pagerCount; //11
    const halfPagerCount = (pagerCount - 1) / 2; //5

    const currentPage = Number(this.currentPage); // 7
    const pageCount = Number(this.pageCount); // 50

    let showPrevMore = false;
    let showNextMore = false;

    //判断是否有more
    if (pageCount > pagerCount) { // 50 > 11
        if (currentPage > pagerCount - halfPagerCount) {
            // 7 > 11 - 5
        showPrevMore = true;
        }

        if (currentPage < pageCount - halfPagerCount) {
            // 7 < 50 - 5
        showNextMore = true;
        }
    }

    const array = [];


    // 如果有more的话,具体是四种情况的哪一种
    if (showPrevMore && !showNextMore) {
        const startPage = pageCount - (pagerCount - 2);
        for (let i = startPage; i < pageCount; i++) {
        array.push(i);
        }
    } else if (!showPrevMore && showNextMore) {
        for (let i = 2; i < pagerCount; i++) {
        array.push(i);
        }
    } else if (showPrevMore && showNextMore) {
        // offset 4
        const offset = Math.floor(pagerCount / 2) - 1;
        // for (let i = 3;i <= 11;i++)
        for (let i = currentPage - offset ; i <= currentPage + offset; i++) {
        array.push(i);
        }
    } else {
        for (let i = 2; i < pageCount; i++) {
        array.push(i);
        }
    }

    this.showPrevMore = showPrevMore;
    this.showNextMore = showNextMore;

    return array;
    }
复制代码

总结所学

目录结构组织

混入 (mixin)

利用事件代理提高性能

watch immediate: true,

computed

文章目前还未完整写全,将会持续更新

文章分类
前端
文章标签