scrollbar组件根目录下包括index.js文件和src文件夹,index.js是用来注册Vue插件的地方,没什么好说的,不了解的童鞋能够看一下Vue官方文档中的 插件,src目录下的内容才是scrollbar组件的核心代码,其入口文件是main.js。javascript
在开始分析源码以前,咱们先来讲一下自定义滚动条的原理,方便你们更好的理解。css
当wrap中的内容溢出的时候,就会产生各浏览器的原生滚动条,要实现自定义滚动条,咱们必须将原生滚动条消灭掉。假设咱们给wrap外面再包一层div,而且把这个div的样式设为overflow:hidden
,同时咱们给wrap的marginRight,marginBottom设置一个负值,值得大小正好等于原生滚动条的宽度,那么这个时候因为父容器的overflow:hidden属性,正好就能够将原生滚动条隐藏掉。而后咱们再将自定义的滚动条绝对定位到wrap容器的右侧和下侧,并加上滚动、拖拽事件等滚动逻辑,就能够实现自定义滚动条了。html
接下来咱们从main.js入口开始,详细分析一下element是如何实现这些逻辑的。vue
main.js文件中直接导出一个对象,这个对象采用render函数的方式渲染scrollbar组件,组件对外暴漏的接口以下:java
props: {
native: Boolean, // 是否采用原生滚动(即只是隐藏掉了原生滚动条,但并无使用自定义的滚动条)
wrapStyle: {}, // 内联方式 自定义wrap容器的样式
wrapClass: {}, // 类名方式 自定义wrap容器的样式
viewClass: {}, // 内联方式 自定义view容器的样式
viewStyle: {}, // 类名方式 自定义view容器的样式
noresize: Boolean, // 若是 container 尺寸不会发生变化,最好设置它能够优化性能
tag: { // view容器用那种标签渲染,默认为div
type: String,
default: 'div'
}
}
复制代码
能够看到,这就是整个ScrollBar组件对外暴露的接口,主要包括了自定义wrap,view样式的接口,以及用来优化性能的noresize接口。node
而后咱们再来分析一下render函数:element-ui
render(){
let gutter = scrollbarWidth(); // 经过scrollbarWidth()方法 获取浏览器原生滚动条的宽度
let style = this.wrapStyle;
if (gutter) {
const gutterWith = `-${gutter}px`;
// 定义即将应用到wrap容器上的marginBottom和marginRight,值为上面求出的浏览器滚动条宽度的负值
const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`;
// 这一部分主要是根据接口wrapStyle传入样式的数据类型来处理style,最终获得的style多是对象或者字符串
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;
}
}
...
}
复制代码
这一块代码中最重要的知识点就是获取浏览器原生滚动条宽度的方式了,为此element专门定义了一个方法scrllbarWidth,这个方法是从外部导入进来的 import scrollbarWidth from 'element-ui/src/utils/scrollbar-width';
,咱们一块儿来看一下这个函数:浏览器
import Vue from 'vue';
let scrollBarWidth;
export default function() {
if (Vue.prototype.$isServer) return 0;
if (scrollBarWidth !== undefined) return scrollBarWidth;
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);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';
const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
scrollBarWidth = widthNoScroll - widthWithScroll;
return scrollBarWidth;
};
复制代码
其实也很简单,就是动态建立一个body的子元素outer,给固定宽度100px,而且将overflow设置为scroll,这样wrap就产生滚动条了,这个时候再动态建立一个outer的子元素inner,将其宽度设置为100%。因为outer有滚动条存在,inner的宽度必然不可能等于outer的宽度,此时用outer的宽度减去inner的宽度,得出的就是浏览器滚动条的宽度了。是否是也很简单啊,最后记得从body中销毁动态建立outer元素哦。微信
回过头来咱们接着看render函数,在根据浏览器滚动条宽度及wrapStyle动态生成样式变量style以后,接下来就是在render函数中生成ScrollBar组件的 HTML了。app
// 生成view节点,而且将默认slots内容插入到view节点下
const view = h(this.tag, {
class: ['el-scrollbar__view', this.viewClass],
style: this.viewStyle,
ref: 'resize'
}, this.$slots.default);
// 生成wrap节点,而且给wrap绑定scroll事件
const wrap = (
<div ref="wrap" style={ style } onScroll={ this.handleScroll } class={ [this.wrapClass, 'el-scrollbar__wrap', gutter ? '' : 'el-scrollbar__wrap--hidden-default'] }> { [view] } </div>
);
复制代码
接着是根据native来组装wrap,view生成整个HTML节点树了。
let nodes;
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>
]);
}
return h('div', { class: 'el-scrollbar' }, nodes);
复制代码
能够看到若是native为false,则使用自定义的滚动条,若是为true,则不使用自定义滚动条。简化上面的render函数生成的HTML以下:
<div class="el-scrollbar">
<div class="el-scrollbar__wrap">
<div class="el-scrollbar__view">
this.$slots.default
</div>
</div>
<Bar vertical move={ this.moveY } size={ this.sizeHeight } />
<Bar move={ this.moveX } size={ this.sizeWidth } />
</div>
复制代码
最外层的el-scrollbar设置了overflow:hidden,用来隐藏wrap中产生的浏览器原生滚动条。使用ScrollBar组建时,写在ScrollBar组件中的内容都将经过slot分发到view内部。另外这里使用move,size和vertical三个接口调用了Bar组件,这个组件就是原理图上的Track和Thumb了。下面咱们来看一下Bar组件:
props: {
vertical: Boolean, // 当前Bar组件是否为垂直滚动条
size: String, // 百分数,当前Bar组件的thumb长度 / track长度的百分比
move: Number // 滚动条向下/向右发生transform: translate的值
},
复制代码
Bar组件的行为都是由这三个接口来进行控制的,在前面的分析中,咱们能够看到,在scrollbar中调用Bar组件时,分别传入了这三个props。那么父组件是如何初始化以及更新这三个参数的值,从而达到更新Bar组件的呢。首先在mounted钩子中调用update方法对size进行初始化:
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 + '%') : '';
}
复制代码
能够看到,这里核心的内容就是计算thumb的长度heightPercentage/widthPercentage。这里使用wrap.clientHeight / wrap.scrollHeight得出了thumb长度的百分比。这是为何呢
分析前面咱们画的那张scrollbar的原理图,thumb在track中上下滚动,可滚动区域view在可视区域wrap中上下滚动,能够将thumb和track的这种相对关系看做是wrap和view相对关系的一个微缩模型(微缩反应),而滚动条的意义就是用来反映view和wrap的这种相对运动关系的。从另外一个角度,咱们能够将view在wrap中的滚动反过来当作是wrap在view中的上下滚动,这不就是一个放大版的滚动条吗?
根据这种类似性,咱们能够得出一个比例关系: wrap.clientHeight / wrap.scrollHeight = thumb.clientHeight / track.clientHeight。在这里,咱们并不须要求出具体的thumb.clientHeight的值,只须要根据thumb.clientHeight / track.clientHeight的比值,来设置thumb 的css高度的百分比就能够了。
另外还有一个须要注意的地方,就是当这个比值大于等于100%的时候,也就是wrap.clientHeight(容器高度)大于等于 wrap.scrollHeight(滚动高度)的时候,此时就不须要滚动条了,所以将size置为空字符串。
接下来咱们再来看一下move,也就是滚动条滚动位置的更新。
handleScroll() {
const wrap = this.wrap;
this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth);
}
复制代码
moveX/moveY用来控制滚动条的滚动位置,当这个值传给Bar组件时,Bar组件render函数中会调用renderThumbStyle
方法将它转化为trumb的样式transform: translateX(${moveX}%)
/ transform: translateY(${moveY}%)
。由以前分析的类似关系可知,当wrap.scrollTop正好等于wrap.clientHeight的时候,此时thumb应该向下滚动它自身长度的距离,也就是transform: translateY(100%)。因此,当wrap滚动的时候,thumb应该向下滚动的距离正好是 transform: translateY(wrap.scrollTop / wrap.clientHeight )。这就是wrap滚动函数handleScroll中的逻辑所在。
如今咱们已经彻底弄清楚了scrollbar组件中的全部逻辑,接下来咱们再看看Bar组件在接收到props以后是如何处理的。
render(h) {
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>
);
}
复制代码
render函数获取父组件传递的size,move以后,经过renderThumbStyle
来生成thumb,而且给track和thumb分别绑定了onMousedown事件。
clickThumbHandler(e) {
this.startDrag(e);
// 记录this.y , this.y = 鼠标按下点到thumb底部的距离
// 记录this.x , this.x = 鼠标按下点到thumb左侧的距离
this[this.bar.axis] = (e.currentTarget[this.bar.offset] - (e[this.bar.client] - e.currentTarget.getBoundingClientRect()[this.bar.direction]));
},
// 开始拖拽函数
startDrag(e) {
e.stopImmediatePropagation();
// 标识位, 标识当前开始拖拽
this.cursorDown = true;
// 绑定mousemove和mouseup事件
on(document, 'mousemove', this.mouseMoveDocumentHandler);
on(document, 'mouseup', this.mouseUpDocumentHandler);
// 解决拖动过程当中页面内容选中的bug
document.onselectstart = () => false;
},
mouseMoveDocumentHandler(e) {
// 判断是否在拖拽过程当中,
if (this.cursorDown === false) return;
// 刚刚记录的this.y(this.x) 的值
const prevPage = this[this.bar.axis];
if (!prevPage) return;
// 鼠标按下的位置在track中的偏移量,即鼠标按下点到track顶部(左侧)的距离
const offset = ((this.$el.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]) * -1);
// 鼠标按下点到thumb顶部(左侧)的距离
const thumbClickPosition = (this.$refs.thumb[this.bar.offset] - prevPage);
// 当前thumb顶部(左侧)到track顶部(左侧)的距离,即thumb向下(向右)偏移的距离 占track高度(宽度)的百分比
const thumbPositionPercentage = ((offset - thumbClickPosition) * 100 / this.$el[this.bar.offset]);
// wrap.scrollHeight / wrap.scrollLeft * thumbPositionPercentage获得wrap.scrollTop / wrap.scrollLeft
// 当wrap.scrollTop(wrap.scrollLeft)发生变化的时候,会触发父组件wrap上绑定的onScroll事件,
// 从而从新计算moveX/moveY的值,这样thumb的滚动位置就会从新渲染
this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
},
mouseUpDocumentHandler(e) {
// 当拖动结束,将标识位设为false
this.cursorDown = false;
// 将上一次拖动记录的this.y(this.x)的值清空
this[this.bar.axis] = 0;
// 取消页面绑定的mousemove事件
off(document, 'mousemove', this.mouseMoveDocumentHandler);
// 清空onselectstart事件绑定的函数
document.onselectstart = null;
}
复制代码
上面的代码就是thumb滚动条拖拽的全部处理逻辑,总体思路就是在拖拽thumb的过程当中,动态的计算thumb顶部(左侧)到track顶部(左侧)的距离占track自己高度(宽度)的百分比,而后利用这个百分比动态改变wrap.scrollTop的值,从而触发页面滚动以及滚动条位置的从新计算,实现滚动效果。
上一个图方便你们理解吧( ̄▽ ̄)"
track的onMousedown和trumb的逻辑也差很少,有两点须要注意:
至此,整个scrollbar源码就分析结束了,回过头来看看,其实scrollbar的实现并不难,主要仍是要理清各类滚动关系、thumb的长度以及滚动位置怎么经过wrap,view之间的关系来肯定。这一部分可能比较绕,没搞懂的同窗建议本身手动画画图研究一下,只要搞懂这个滚动原理,实现起来就很简单了。