深入分析element ScrollBar滚动组件源码

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了。

// 生成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的逻辑也差不多,有两点需要注意:
  • track的onMousedown事件回调中不会给页面绑定mousemove和mouseup事件,因为track相当于click事件 在track的onmousedown事件中,我们计算thumb顶部到track顶部的方法是,用鼠标点击点到track顶部的距离减去thumb的二分之一高度,这是因为点击track之后,thumb的中点刚好要在鼠标点击点的位置。

至此,整个scrollbar源码就分析结束了,回过头来看看,其实scrollbar的实现并不难,主要还是要理清各种滚动关系、thumb的长度以及滚动位置怎么通过wrap,view之间的关系来确定。这一部分可能比较绕,没搞懂的同学建议自己手动画画图研究一下,只要搞懂这个滚动原理,实现起来就很简单了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Element-ui之ElScrollBar组件滚动条的使用方法

    在使用vue + element-ui 搭建后台管理页面的时候,做了一个头部.侧栏.面包屑固定的布局,导航栏和主要内容区域当内容超出时自动滚动. 使用的原因: 原来是采用优化浏览器样式的方式,对滚动条进行样式调整.但这个方法并不兼容火狐浏览器,在火狐访问时依然是浏览器默认的滚动条样式. .sidebar { position: fixed; border-right: 1px solid rgba(0,0,0,.07); overflow-y: auto; position: absolute;

  • 深入分析element ScrollBar滚动组件源码

    scrollbar组件根目录下包括index.js文件和src文件夹,index.js是用来注册Vue插件的地方,没什么好说的,不了解的童鞋可以看一下Vue官方文档中的插件,src目录下的内容才是scrollbar组件的核心代码,其入口文件是main.js. 在开始分析源码之前,我们先来说一下自定义滚动条的原理,方便大家更好的理解. 如图,黑色wrap为滚动的可显示区域,我们的滚动内容就是在这个区域中滚动,view是实际的滚动内容,超出wrap可显示区域的内容都将被隐藏.右侧track是滚动条的

  • Spring Boot 深入分析AutoConfigurationImportFilter自动化条件配置源码

    目录 1. AutoConfigurationImportFilter的作用 2. AutoConfigurationImportFilter UML类图说明 3. FilteringSpringBootCondition抽象类 4. AutoConfigurationImportSelector类 5. 总结 1. AutoConfigurationImportFilter的作用 之前讲解了SpringBoot的Conditional的自动化条件配置,我们分析了内部是如何具体实现,在整个实现当

  • JavaScript实现仿新浪微博大厅和腾讯微博首页滚动特效源码

    JavaScript实现仿新浪微博大厅和未登录腾讯微博首页滚动效果,貌似这些天有不少朋友需要这功能,前几天已经发了几个了,不过那一个效果是用jquery实现的<jQuery向上循环滚动(仿新浪微博未登录首页滚动微博显示)>,今天发现这款是js制作的好,不敢独享,希望需要的朋友喜欢哦. 效果图展示如下: 查看演示         源码下载 html代码 <div class="wp"> <ul id="slider" class=&quo

  • vue实现简单的星级评分组件源码

    vue星级评分组件源码,具体代码如下所示: <!--自定义组件--> <template> <!--结构层--> <div id="star"> <span v-for="classn in starArrs" :class="classn" class="staritem"></span> <!--<span class="star

  • JavaScript实现简单图片滚动附源码下载

    昨晚德国和葡萄牙的焦点之战你看了吗?北京时间凌晨的比赛中,C罗领衔的葡萄牙0-4德国被完灭--他是金球奖得主.欧洲金靴.欧冠冠军核心,在葡萄牙队--9张图 C罗告诉你什么叫欲哭无泪 复制代码 代码如下: <span style="font-size:14px;"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtm

  • React-View-UI组件库封装Loading加载中源码

    目录 组件介绍 Loading API能力 组件源码 组件测试源码 组件库线上地址 组件介绍 Loading组件是日常开发用的很多的组件,这次封装主要包含两种状态的Loading,旋转.省略号,话不多说先看一下组件的文档页面吧: Loading API能力 组件一共提供了如下的API能力,可以在使用时更灵活: type表示loading类型,默认是default,当用户需要使用省略样式,设置type=dot即可: mask配置蒙层,可在loading时遮挡覆盖内容为半透明状态,适用于内容未加载时

  • Ajax::prototype 源码解读

    AJAX之旅(1):由prototype_1.3.1进入javascript殿堂-类的初探  还是决定冠上ajax的头衔,毕竟很多人会用这个关键词搜索.虽然我认为这只是个炒作的概念,不过不得不承认ajax叫起来要方便多了.所以ajax的意思我就不详细解释了. 写这个教程的起因很简单:经过一段时间的ajax学习,有一些体会,并且越发认识到ajax技术的强大,所以决定记录下来,顺便也是对自己思路的整理.有关这个教程的后续,请关注http://www.x2design.net 前几年,javascri

  • Android 实现IOS 滚轮选择控件的实例(源码下载)

     Android 实现IOS 滚轮选择控件的实例 最近根据项目需要,整理了一个相对比较全面的 WheelView 使用控件,借用之前看到的一句话来说,就是站在巨人肩膀上,进行了一些小调整. 这里先贴上效果图 一般常用的时间选择格式,,单项选择,以及城市联动,这里基本都可以满足了. 这里把 单项选择,和 日期时间选择 给提出到 Util 类中,代码如下: public class Util { /** * 时间选择回调 */ public interface TimerPickerCallBack

  • Flutter加载图片流程之ImageCache源码示例解析

    目录 ImageCache _pendingImages._cache._liveImages maximumSize.currentSize clear evict _touch _checkCacheSize _trackLiveImage putIfAbsent clearLiveImages 答疑解惑 ImageCache const int _kDefaultSize = 1000; const int _kDefaultSizeBytes = 100 << 20; // 100 M

  • element日历calendar组件上月、今天、下月、日历块点击事件及模板源码

    辰小白小白最近在写日历模板,项目已经用了element组件,奈何element日历组件官方文档提供的资料实在太少了.所以这里希望有相关开发需要的朋友能够少走一些辰小白踩过的坑. 首先展示一些模板效果图: 这个项目的详细介绍可以下辰小白的这篇文章:后端开发的福音,vue+element实现的vue-element-admin前台框架,开箱即用 下面是日历模板首页源码 <template> <div> <el-card class="_calendar">

随机推荐