基于vue.js 2.x的虛擬滾動條的示例代碼
前言
記得以前偶然有一次瀏覽過一個開源的cms項目,發(fā)現(xiàn)這個項目的左邊的菜單已經(jīng)超出了windows的寬度,我就好奇為什么沒出滾動條呢?然后我仔細一看,發(fā)現(xiàn)它左側(cè)有一個小的div,然后我嘗試著拖動它,發(fā)現(xiàn)竟能和原生的滾動條一樣!可以通過查看它的源碼,發(fā)現(xiàn)了這款滾動條的叫做slimScroll,然后我去它的github倉庫 看了下,研究了一下源碼,給我的感覺是我也能做出來一樣的滾動條!通過vue實現(xiàn)!
設(shè)計
好, 現(xiàn)在開始我們的設(shè)計滾動條的步驟:
設(shè)計滾動條dom
首先要思考的是: 如果要使你需要滾動的內(nèi)容滾動的話,首先一點是它的父dom必須為固定長寬,即超出部分要隱藏掉,即加了個樣式: overflow: hidden , 所以,我們給所要滾動的內(nèi)容加個包裝,使它的長寬和父dom相等,然后有一個樣式叫: overflow: hidden ,這個包裝的元素就叫 scrollPanel
其次:我們知道,我們要做到與原生滾動條一樣強大!就必須設(shè)計水平滾動條和垂直滾動條,滾動條和scrollPanel屬于兄弟節(jié)點之間的關(guān)系,因為滾動條的存在不能使原本的樣式排版錯誤,并且支持top、left來控制其位置,所以滾動條的position必須是absolute,好了,我們叫水平滾動條為:hBar,垂直滾動條為:vBar
最后:我們設(shè)計了scrollPanel、vBar、hBar, 我們需要一個父div來把他們包裝起來,然后加個樣式:position: relative
實踐
設(shè)計組件結(jié)構(gòu)
首先,我們的插件一共是4個組件,其中3個是子組件,1個是父組件,分別是: vueScroll (父組件)、 scrollPanel (包裹需要滾動內(nèi)容的子組件)、 vBar (垂直滾動條)、 hBar (水平滾動條)
其次,讓我們設(shè)計一下各組件所分管的功能。這里的組件分為控制層組件和展示組件(熟悉react的同學(xué)應(yīng)該有所了解),展示層組件只完成展示的功能: vBar 、 hBar 、 scrollPanel ,控制層組件有點類似于cpu,可以控制子組件的各個狀態(tài),比如寬、高、顏色、透明度、位置等等??刂茖咏M件就是: vueScroll 。
具體實現(xiàn)
hBar/vBar
hBar/vBar 這兩個分別為水平滾動條和垂直滾動條,所實現(xiàn)的功能大體是一樣的,所以舊放在一起說了,這里以 vBar 為例。
props 接收父組件傳過來的屬性,具體為:
{
height: vm.state.height + 'px', //滾動條的高度
width: vm.ops.width, // 滾動條的寬度
position: 'absolute',
background: vm.ops.background, // 滾動條背景色
top: vm.state.top + 'px', // 滾動條的高度
transition: 'opacity .5s', // 消失/顯示 所用的時間
cursor: 'pointer', //
opacity: vm.state.opacity, // 透明度
userSelect: 'none'
}
2 事件,主要是當(dāng)鼠標(biāo)移動的時候,顯示滾動條。
...
render(_c){
return _c(
// ...
{
mouseenter: function(e) {
vm.$emit('showVBar'); // 觸發(fā)父組件事件,顯示滾動條
}
}
// ...
)
}
其中 state 表示狀態(tài),是在運行時可發(fā)生改變的,而 ops 則是配置參數(shù),是用戶傳過來的。
scrollPanel
包裹滾動內(nèi)容的組件,樣式需設(shè)置為: overflow: hidden 。
1、樣式
var style = vm.scrollContentStyle;
style.overflow = 'hidden';
// ...
{
style: style
}
// ...
2、事件
// ...
render(_c) {
// ...
on: {
mouseenter: function() {
vm.$emit('showBar');
},
mouseleave: function() {
vm.$emit('hideBar');
}
}
// ...
}
// ...
vuescroll
控制組件。控制子組件顯示的狀態(tài),添加各種監(jiān)聽事件等。
1、取得子組件的dom元素,用來取得dom的實時信息。
// ...
initEl() {
this.scrollPanel.el = this.$refs['vueScrollPanel'] && this.$refs['vueScrollPanel'].$el;
this.vScrollBar.el = this.$refs['vScrollBar'] && this.$refs['vScrollBar'].$el;
this.hScrollBar.el = this.$refs['hScrollBar'] && this.$refs['hScrollBar'].$el;
}
// ...
2、顯示滾動條
顯示滾動條,包括顯示水平滾動條和顯示垂直滾動條,這里以顯示垂直滾動條為例:
// ...
var temp;
var deltaY = {
deltaY: this.vScrollBar.ops.deltaY // 獲取用戶配置的deltaY
};
if(!this.isMouseLeavePanel || this.vScrollBar.ops.keepShow){
if ((this.vScrollBar.state.height = temp = this.getVBarHeight(deltaY))) { // 判斷條件
// 重新設(shè)置滾動條的狀態(tài)
this.vScrollBar.state.top = this.resizeVBarTop(temp);
this.vScrollBar.state.height = temp.height;
this.vScrollBar.state.opacity = this.vScrollBar.ops.opacity;
}
}
// ...
3、獲取滾動條的高度
因為dom元素的高度不是固定的,所以你要實時地獲取dom真實的高度,滾動條的高度計算公式如下:
var height = Math.max(
scrollPanelHeight /
(scrollPanelScrollHeight / scrollPanelHeight),
this.vScrollBar.minBarHeight
);
即: 滾動條的高度:scrollPanel的高度 == scrollPanel的高度:dom元素高度
4、resizeVBarTop ,為了防止誤差,并且可以求出滾動條距離父元素的高度。
resizeVBarTop({height, scrollPanelHeight, scrollPanelScrollHeight, deltaY}) {
// cacl the last height first
var lastHeight = scrollPanelScrollHeight - scrollPanelHeight - this.scrollPanel.el.scrollTop;
if(lastHeight < this.accuracy) {
lastHeight = 0;
}
var time = Math.abs(Math.ceil(lastHeight / deltaY));
var top = scrollPanelHeight - (height + (time * this.vScrollBar.innerDeltaY));
return top;
}
5、監(jiān)聽滾輪滾動的事件。
// ...
on: {
wheel: vm.wheel
}
// ...
wheel(e) {
var vm = this;
vm.showVBar();
vm.scrollVBar(e.deltaY > 0 ? 1 : -1, 1);
e.stopPropagation();
}
// ...
6、監(jiān)聽滾動條拖拽事件
listenVBarDrag: function() {
var vm = this;
var y;
var _y;
function move(e) {
_y = e.pageY;
var _delta = _y - y;
vm.scrollVBar(_delta > 0 ? 1 : -1, Math.abs(_delta / vm.vScrollBar.innerDeltaY));
y = _y;
}
function t(e) {
var deltaY = {
deltaY: vm.vScrollBar.ops.deltaY
};
if(!vm.getVBarHeight(deltaY)) {
return;
}
vm.mousedown = true;
y = e.pageY; // 記錄初始的Y的位置
vm.showVBar();
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', function(e) {
vm.mousedown = false;
vm.hideVBar();
document.removeEventListener('mousemove', move);
});
}
this.listeners.push({
dom: vm.vScrollBar.el,
event: t,
type: "mousedown"
});
vm.vScrollBar.el.addEventListener('mousedown', t); // 把事件放到數(shù)組里面,等銷毀之前移除掉注冊的時間。
}
7、適配移動端,監(jiān)聽 touch 事件。原理跟拖拽事件差不多,無非就是多了個判斷,來判斷當(dāng)前方向是x還是y。
listenPanelTouch: function() {
var vm = this;
var pannel = this.scrollPanel.el;
var x, y;
var _x, _y;
function move(e) {
if(e.touches.length) {
var touch = e.touches[0];
_x = touch.pageX;
_y = touch.pageY;
var _delta = void 0;
var _deltaX = _x - x;
var _deltaY = _y - y;
if(Math.abs(_deltaX) > Math.abs(_deltaY)) {
_delta = _deltaX;
vm.scrollHBar(_delta > 0 ? -1 : 1, Math.abs(_delta / vm.hScrollBar.innerDeltaX));
} else if(Math.abs(_deltaX) < Math.abs(_deltaY)){
_delta = _deltaY;
vm.scrollVBar(_delta > 0 ? -1 : 1, Math.abs(_delta / vm.vScrollBar.innerDeltaY));
}
x = _x;
y = _y;
}
}
function t(e) {
var deltaY = {
deltaY: vm.vScrollBar.ops.deltaY
};
var deltaX = {
deltaX: vm.hScrollBar.ops.deltaX
};
if(!vm.getHBarWidth(deltaX) && !vm.getVBarHeight(deltaY)) {
return;
}
if(e.touches.length) {
e.stopPropagation();
var touch = e.touches[0];
vm.mousedown = true;
x = touch.pageX;
y = touch.pageY;
vm.showBar();
pannel.addEventListener('touchmove', move);
pannel.addEventListener('touchend', function(e) {
vm.mousedown = false;
vm.hideBar();
pannel.removeEventListener('touchmove', move);
});
}
}
pannel.addEventListener('touchstart', t);
this.listeners.push({
dom: pannel,
event: t,
type: "touchstart"
});
}
8、滾動內(nèi)容
滾動內(nèi)容的原理無非就是改變 scrollPanel 的 scrollTop/scrollLeft 來達到控制內(nèi)容上下左右移動的目的。
scrollVBar: function(pos, time) {
// >0 scroll to down <0 scroll to up
var top = this.vScrollBar.state.top;
var scrollPanelHeight = getComputed(this.scrollPanel.el, 'height').replace('px', "");
var scrollPanelScrollHeight = this.scrollPanel.el.scrollHeight;
var scrollPanelScrollTop = this.scrollPanel.el.scrollTop;
var height = this.vScrollBar.state.height;
var innerdeltaY = this.vScrollBar.innerDeltaY;
var deltaY = this.vScrollBar.ops.deltaY;
if (!((pos < 0 && top <= 0) || (scrollPanelHeight <= top + height && pos > 0) || (Math.abs(scrollPanelScrollHeight - scrollPanelHeight) < this.accuracy))) {
var Top = top + pos * innerdeltaY * time;
var ScrollTop = scrollPanelScrollTop + pos * deltaY * time;
if (pos < 0) {
// scroll ip
this.vScrollBar.state.top = Math.max(0, Top);
this.scrollPanel.el.scrollTop = Math.max(0, ScrollTop);
} else if (pos > 0) {
// scroll down
this.vScrollBar.state.top = Math.min(scrollPanelHeight - height, Top);
this.scrollPanel.el.scrollTop = Math.min(scrollPanelScrollHeight - scrollPanelHeight, ScrollTop);
}
}
// 這些是傳遞給父組件的監(jiān)聽滾動的函數(shù)的。
var content = {};
var bar = {};
var process = "";
content.residual = (scrollPanelScrollHeight - scrollPanelScrollTop - scrollPanelHeight);
content.scrolled = scrollPanelScrollTop;
bar.scrolled = this.vScrollBar.state.top;
bar.residual = (scrollPanelHeight - this.vScrollBar.state.top - this.vScrollBar.state.height);
bar.height = this.vScrollBar.state.height;
process = bar.scrolled/(scrollPanelHeight - bar.height);
bar.name = "vBar";
content.name = "content";
this.$emit('vscroll', bar, content, process);
},
9、銷毀注冊的事件。
剛才我們已經(jīng)把注冊事件放到listeners數(shù)組里面了,我們可以在beforedestroy鉤子里將他們進行銷毀。
// remove the registryed event.
this.listeners.forEach(function(item) {
item.dom.removeEventListener(item.event, item.type);
});
運行截圖
PC端運行截圖如下圖所示:

注冊監(jiān)聽事件以后如下圖所示:

在手機上運行截圖:

可以看出,跟原生滾動條表現(xiàn)效果一致。
結(jié)語&感悟
以上就基本把我設(shè)計的滾動條設(shè)計完了,首先很感激掘金給了我這么一個分享平臺,然后感謝slimScroll的作者給了我這么一個思路。做完這個插件, 我對dom元素的scrollWidth、scrollHeigh、scrollTop、scrollLeft的了解更多了,最后,附上github項目地址
以上部分就是這個組件的核心源碼了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue2.0在table中實現(xiàn)全選和反選的示例代碼
這篇文章主要介紹了vue2.0在table中實現(xiàn)全選和反選的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11
Vue.extend 登錄注冊模態(tài)框的實現(xiàn)
這篇文章主要介紹了Vue.extend 登錄注冊模態(tài)框的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
詳解在WebStorm中添加Vue.js單文件組件的高亮及語法支持
本篇文章主要介紹了詳解在WebStorm中添加Vue.js單文件組件的高亮及語法支持,非常具有實用價值,需要的朋友可以參考下2017-10-10
vue之el-tree懶加載數(shù)據(jù)并且實現(xiàn)樹的過濾問題
這篇文章主要介紹了vue之el-tree懶加載數(shù)據(jù)并且實現(xiàn)樹的過濾問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04
如何使用Vuex+Vue.js構(gòu)建單頁應(yīng)用
這篇文章主要教大家如何使用Vuex+Vue.js構(gòu)建單頁應(yīng)用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10

