// 生成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> <div> <div> 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之后是如何处理的。