scrollbar组件根目录下包括index.js文件和src文件夹,index.js是用来注册Vue插件的地方,没什么好说的,不了解的童鞋可以看一下Vue官方文档中的插件,src目录下的内容才是scrollbar组件的核心代码,其入口文件是main.js。
在开始分析源码之前,我们先来说一下自定义滚动条的原理,方便大家更好的理解。
如图,黑色wrap为滚动的可显示区域,我们的滚动内容就是在这个区域中滚动,view是实际的滚动内容,超出wrap可显示区域的内容都将被隐藏。右侧track是滚动条的滚动滑块thumb上下滚动的轨迹
当wrap中的内容溢出的时候,就会产生各浏览器的原生滚动条,要实现自定义滚动条,我们必须将原生滚动条消灭掉。假设我们给wrap外面再包一层div,并且把这个div的样式设为 overflow:hidden ,同时我们给wrap的marginRight,marginBottom设置一个负值,值得大小正好等于原生滚动条的宽度,那么这个时候由于父容器的overflow:hidden属性,正好就可以将原生滚动条隐藏掉。然后我们再将自定义的滚动条绝对定位到wrap容器的右侧和下侧,并加上滚动、拖拽事件等滚动逻辑,就可以实现自定义滚动条了。
接下来我们从main.js入口开始,详细分析一下element是如何实现这些逻辑的。
main.js文件中直接导出一个对象,这个对象采用render函数的方式渲染scrollbar组件,组件对外暴漏的接口如下:
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接口。
然后我们再来分析一下render函数:
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了。