遮遮掩掩的滚动条el-scrollbar

el-scrollbar 是啥?

Element-UI,作为一套非常出名 Vue 的 UI 组件库,玩 Vue 人几乎都认识它。最近在翻看 Element 的源码时,发现了一个有趣的现象,怎么 autocomplete 组件的联想列表组件 -> autocomplete-suggestions 里面,还包了一个 el-scrollbar 组件,这是用来做什么的?

经过一番了解,原来是 Element 自己写的一个滚动条组件(但却没有公开发布出来),它屏蔽了原生的滚动条,使用了一个统一的样式来代替,解决了滚动条的兼容性问题。

如何使用?

关于 el-scrollbar 的使用方式,可以看 Github 上的 issues,这里也简单展示一下:在 el-scrollbar 的默认 slot 中填入一个列表,并设定最外层的包裹元素的高度,这样就能顺利产生滚动条了。

<template>

// 这里的 tag 属性可以先忽略,它用于控制生成的view元素具体是什么类型的元素

<el-scrollbar tag="ul">

<li>1</li>

<li>2</li>

<li>3</li>

<li>4</li>

</el-scrollbar>

</template>

效果如下:

image.png

如何实现?

先来看刚刚的代码渲染出来的DOM:

image.png

可以看到,我们的 li 被包裹在了 .el-scrollbar -> .&__wrap -> .&__view 里面,而底下还有两个 DOM:.is-horizontal 和 .is-vertical ,每个元素都有他自己的作用:

<div class="el-scrollbar"> //根元素,包裹所有元素

<div class="el-scrollbar__wrap"> // wrap 元素,是视觉视口元素,它代表着元素最终展示的窗口大小

<ul class="el-scrollbar__view"> // 布局视口元素,它代表着整个列表(以及他们的宽高),通过调整 wrap 的scrollTop/left,显示不同的 view 内容

// 默认插槽里的内容会被放在这里

</ul>

</div>

<div class="el-scrollbar__bar is-horizontal">...</div> //横向滚动条

<div class="el-scrollbar__bar is-vertical">...</div> // 竖向滚动条

</div>

隐藏原有滚动条

了解了wrap/view/bar这几个概念之后,我们直接来看源码: element/packages/scrollbar/src/main.js 这个文件是 scrollbar 组件的入口文件,它定义了一些/components/data/接受的 props,以及最重要的:render 函数。render 函数在被调用的时候,首先调用了 scrollbarWidth 函数:

let gutter = scrollbarWidth();

这个 gutter 的意思是当前浏览器的滚动条宽度,element 通过 scrollbarWidth 这个方法来获取到这个宽度,点击这个方法,可以看到其实它做了三件事情:

  1. 创建了一个 outer 元素,设置了宽度,拿到此时的 offsetWidth
  2. 把 outer 元素 overflow 设置为 visible,再创建一个inner元素,append 到 outer 上(此时会产生滚动条),再拿到 inner 的 offsetWidth。
  3. 两者相减即是滚动条的宽度

/* eslint-disable no-debugger */

import Vue from 'vue';

let scrollBarWidth;

export default function() {

if (Vue.prototype.$isServer) return 0;

if (scrollBarWidth !== undefined) return scrollBarWidth;

// 创建外层的div,此时是一个普通的dom

const outer = document.createElement('div');

outer.className = 'el-scrollbar__wrap';

outer.style.visibility = 'hidden';

outer.style.width = '100px';

outer.style.position = 'absolute';

outer.style.top = '-9999px';

document.body.appendChild(outer);

// 获取这个dom的实际宽度

const widthNoScroll = outer.offsetWidth;

// 修改外层 dom 的css,设置为 overflow: scroll(默认产生滚动条)

outer.style.overflow = 'scroll';

// 创建内层的 div,并 append 到 outer 上

const inner = document.createElement('div');

inner.style.width = '100%';

outer.appendChild(inner);

// 计算内层 div 的实际宽度

const widthWithScroll = inner.offsetWidth;

outer.parentNode.removeChild(outer);

// 通过「无滚动条时的宽度」减去「有滚动条时的宽度」来算出滚动条的具体宽度

scrollBarWidth = widthNoScroll - widthWithScroll;

return scrollBarWidth;

};

拿到了滚动条最主要的目的就是为了把它隐藏掉,这也是 render 函数接下来做的事情。

const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`;

// 根据传入的 wrapStyle 的不同类型,把 gutterStyle 加入进去

if (Array.isArray(this.wrapStyle)) {

style = toObject(this.wrapStyle);

style.marginRight = style.marginBottom = gutterWith;

} else if (typeof this.wrapStyle === 'string') {

style += gutterStyle;

} else {

style = gutterStyle;

}

}

创建 DOM

紧接着就是 DOM 的创建过程,先后创建了 view/wrap(监听其滚动事件),以及非原生版本/原生版本的根元素。如果你传入了 native: true,就代表着使用了原生滚动条版本的 scrollbar。

    if (!this.native) {

nodes = ([

wrap,

<Bar

move={ this.moveX }

size={ this.sizeWidth }></Bar>,

<Bar

vertical

move={ this.moveY }

size={ this.sizeHeight }></Bar>

]);

} else {

nodes = ([

<div

ref="wrap"

class={ [this.wrapClass, 'el-scrollbar__wrap'] }

style={ style }>

{ [view] }

</div>

]);

}

在 wrap 窗口滚动时,handleScroll 方法会被执行,更新 data 中的 moveY 和 moveX 属性。这两者会被传入滚动条组件 Bar ,更新它的 translateY()/translateX() ,Bar 组件我们后面会讲到。

mount/beforeDestroy 钩子

在 mounted 的时候还做了一件事,就是给 view 元素添加了 resize 事件的监听器(beforeDestroy 时取消监听):

!this.noresize && addResizeListener(this.$refs.resize, this.update);

值得注意的是,addResizeListener 并不是简单地设置了 window.resize 回调,而是使用了一个船新的 api 来监听 DOM 元素的 resize:ResizeObserver API(具体可看这里的介绍)。总的来说,ResizeObserver 可以直接给 DOM 绑定事件,专门用来观察 DOM 元素的尺寸是否发生了变化,减少了 window.resize 带来的多余监听。

为了给某个元素实现多个 resize 事件的监听,element 还使用了观察者模式,给 DOM 元素绑定了一个 __resizeListeners__ 数组,当有 resize 事件被触发时,执行整个 _ _resizeListeners__ 数组的所有回调。

DOM 元素一旦 resize,就会执行 update 回调。那么 update 的时候做了什么事情呢?

update() {

let heightPercentage, widthPercentage;

const wrap = this.wrap;

if (!wrap) return;

// 得到新的宽高占比

heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight);

widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth);

this.sizeHeight = (heightPercentage < 100) ? (heightPercentage + '%') : '';

this.sizeWidth = (widthPercentage < 100) ? (widthPercentage + '%') : '';

}

update 方法负责更新 Bar 的滑块长度(可能是横向/竖向滚动条),我们以竖向滚动条为例:首先通过 clientHeight * 100/scrollHeight 得到 resize 后的 wrap 展示高度和总高度的比例,这也是 scrollbar 滑块长度的比例,再把它传入给表示滚动条的 Bar 组件,更新滚动条的 height。

这个时候如果比例值大于 100,说明已经不需要滚动条了,则传一个空字符串给 Bar 。

点击/拖动滚动条

到了这一步,我们的滚动条组件已经创建完成了,但是我们点击滚动条或者拖动滚动条的时候,这个组件如何处理呢?还得看 element/packages/scrollbar/src/bar.js 这个组件。

Bar 组件负责展示滚动条,我们直接来看它的 render 函数:

  render(h) {

// move 属性用于控制滚动条的滚动位置

const { size, move, bar } = this;

return (

<div

class={ ['el-scrollbar__bar', 'is-' + bar.key] }

onMousedown={ this.clickTrackHandler } >

<div

ref="thumb"

class="el-scrollbar__thumb"

onMousedown={ this.clickThumbHandler }

style={ renderThumbStyle({ size, move, bar }) }>

</div>

</div>

);

}

我们可以看到重点在于 clickTrackHandler/clickThumbHandler 这两个函数,他们分别用于控制滚动条 container 被点击时的行为,以及滚动条本身被点击的时候产生的行为。

clickTrackHandler:快速跳到某个区间

clickTrackHandler(e) {

/**

* 0. 以垂直滚动条为例:

* this.bar.direction = "top"/this.bar.client = "clientY"/this.bar.offset="offsetHeight"/this.bar.scrollSize="scrollHeight"

* 1. getBoundingClientRect()[this.bar.direction] 返回元素的 top 值(距离浏览器视口的高度值)

* 2. 用 1 的值减去 e.clientY(鼠标当前位置), 再用 Math.abs 得出相对值,这个值就是鼠标在滚动条 container 上的相对偏移量。

* 3. 计算出滚动条滑块的一半位置 thumbHalf

* 4. offset - thumbHalf 得到具体偏移量,并除以整个 bar 的 offsetHeight,得到了滑块新的位置的百分比。

* 5. 接下来就可以愉快地更新 wrap 元素的 scrollTop,显示新的内容啦~

* 6. wrap 滚动后会触发 handleScroll 方法,回过头来更新 Bar 组件的 move 值,从而更新滚动条位置。

*/

const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]);

const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);

// 计算点击后,根据 偏移量 计算在 滚动条区域的总高度 中的占比,也就是 滚动块 所处的位置

const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]);

// 设置外壳的 scrollHeight 或 scrollWidth 新值。达到滚动内容的效果

this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);

},

clickThumbHandler:拖动滚动条滑块更新视图

这里主要是计算拖动时滑块的高度与整个滚动条的比例,从而更新 wrap 元素的 scrollTop 值,具体代码与 clickTrackHandler 较为相似,由于篇幅所限,就不赘述了。

这里有一个小点,我们是给滑块元素绑定 onMousedown 事件的,但是 mousemove 和 mouseup 却是绑定在 document 上的,这是因为鼠标在移动过程中,会比滑块的移动要快,此时滑块元素会失去 onMousemove 事件,所以绑定 mousemove 的时候不能绑定在对应元素上。

总结

我们从整个滚动条元素的生命周期,看到 element 是如何创建出一个滚动条,如何监听元素的变化,如何控制滚动条的滑动等等。源码的阅读到这里就全部结束了,如有什么错漏,请帮忙指出来;如你有所收获,是我莫大的荣幸。

以上是 遮遮掩掩的滚动条el-scrollbar 的全部内容, 来源链接: utcz.com/a/22756.html

回到顶部