Element-UI,做爲一套很是出名 Vue 的 UI 組件庫,玩 Vue 人幾乎都認識它。最近在翻看 Element 的源碼時,發現了一個有趣的現象,怎麼 autocomplete 組件的聯想列表組件 -> autocomplete-suggestions 裏面,還包了一個 el-scrollbar 組件,這是用來作什麼的?
通過一番瞭解,原來是 Element 本身寫的一個滾動條組件(但卻沒有公開發布出來),它屏蔽了原生的滾動條,使用了一個統一的樣式來代替,解決了滾動條的兼容性問題。javascript
關於 el-scrollbar 的使用方式,能夠看 Github 上的 issues,這裏也簡單展現一下:在 el-scrollbar 的默認 slot 中填入一個列表,並設定最外層的包裹元素的高度,這樣就能順利產生滾動條了。css
<template> // 這裏的 tag 屬性能夠先忽略,它用於控制生成的view元素具體是什麼類型的元素 <el-scrollbar style="width: 150px; height: 50px" tag="ul"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </el-scrollbar> </template>
效果以下:
vue
先來看剛剛的代碼渲染出來的DOM:
能夠看到,咱們的 li
被包裹在了 .el-scrollbar -> .&__wrap -> .&__view
裏面,而底下還有兩個 DOM:.is-horizontal
和 .is-vertical
,每一個元素都有他本身的做用:java
<div class="el-scrollbar"> //根元素,包裹全部元素 <div class="el-scrollbar__wrap"> // wrap 元素,是視覺視口元素,它表明着元素最終展現的窗口大小 <ul class="el-scrollbar__view"> // 佈局視口元素,它表明着整個列表(以及他們的寬高),經過調整 wrap 的scrollTop/left,顯示不一樣的 view 內容 // 默認插槽裏的內容會被放在這裏 </ul> </div> <div class="el-scrollbar__bar is-horizontal">...</div> //橫向滾動條 <div class="el-scrollbar__bar is-vertical">...</div> // 豎向滾動條 </div>
瞭解了wrap/view/bar這幾個概念以後,咱們直接來看源碼: element/packages/scrollbar/src/main.js
這個文件是 scrollbar 組件的入口文件,它定義了一些/components/data/接受的 props,以及最重要的:render 函數。render 函數在被調用的時候,首先調用了 scrollbarWidth 函數:node
let gutter = scrollbarWidth();
這個 gutter 的意思是當前瀏覽器的滾動條寬度,element 經過 scrollbarWidth 這個方法來獲取到這個寬度,點擊這個方法,能夠看到其實它作了三件事情:git
/* eslint-disable no-debugger */ import Vue from 'vue'; let scrollBarWidth; export default function() { if (Vue.prototype.$isServer) return 0; if (scrollBarWidth !== undefined) return scrollBarWidth; // 建立外層的div,此時是一個普通的dom 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); // 獲取這個dom的實際寬度 const widthNoScroll = outer.offsetWidth; // 修改外層 dom 的css,設置爲 overflow: scroll(默認產生滾動條) outer.style.overflow = 'scroll'; // 建立內層的 div,並 append 到 outer 上 const inner = document.createElement('div'); inner.style.width = '100%'; outer.appendChild(inner); // 計算內層 div 的實際寬度 const widthWithScroll = inner.offsetWidth; outer.parentNode.removeChild(outer); // 經過「無滾動條時的寬度」減去「有滾動條時的寬度」來算出滾動條的具體寬度 scrollBarWidth = widthNoScroll - widthWithScroll; return scrollBarWidth; };
拿到了滾動條最主要的目的就是爲了把它隱藏掉,這也是 render 函數接下來作的事情。github
const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`; // 根據傳入的 wrapStyle 的不一樣類型,把 gutterStyle 加入進去 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; } }
緊接着就是 DOM 的建立過程,前後建立了 view/wrap(監聽其滾動事件),以及非原生版本/原生版本的根元素。若是你傳入了 native: true
,就表明着使用了原生滾動條版本的 scrollbar。segmentfault
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> ]); }
在 wrap 窗口滾動時,handleScroll 方法會被執行,更新 data 中的 moveY 和 moveX 屬性。這二者會被傳入滾動條組件 Bar
,更新它的 translateY()/translateX()
,Bar 組件咱們後面會講到。api
在 mounted 的時候還作了一件事,就是給 view 元素添加了 resize 事件的監聽器(beforeDestroy 時取消監聽):數組
!this.noresize && addResizeListener(this.$refs.resize, this.update);
值得注意的是,addResizeListener 並非簡單地設置了 window.resize 回調,而是使用了一個船新的 api 來監聽 DOM 元素的 resize:ResizeObserver API(具體可看這裏的介紹)。總的來講,ResizeObserver 能夠直接給 DOM 綁定事件,專門用來觀察 DOM 元素的尺寸是否發生了變化,減小了 window.resize 帶來的多餘監聽。
爲了給某個元素實現多個 resize 事件的監聽,element 還使用了觀察者模式,給 DOM 元素綁定了一個 __resizeListeners__
數組,當有 resize 事件被觸發時,執行整個 _
_resizeListeners_
_ 數組的全部回調。
DOM 元素一旦 resize,就會執行 update 回調。那麼 update 的時候作了什麼事情呢?
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 + '%') : ''; }
update 方法負責更新 Bar 的滑塊長度(多是橫向/豎向滾動條),咱們以豎向滾動條爲例:首先經過 clientHeight * 100/scrollHeight
獲得 resize 後的 wrap 展現高度和總高度的比例,這也是 scrollbar 滑塊長度的比例,再把它傳入給表示滾動條的 Bar
組件,更新滾動條的 height。
這個時候若是比例值大於 100,說明已經不須要滾動條了,則傳一個空字符串給 Bar
。
到了這一步,咱們的滾動條組件已經建立完成了,可是咱們點擊滾動條或者拖動滾動條的時候,這個組件如何處理呢?還得看 element/packages/scrollbar/src/bar.js
這個組件。
Bar 組件負責展現滾動條,咱們直接來看它的 render 函數:
render(h) { // move 屬性用於控制滾動條的滾動位置 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> ); }
咱們能夠看到重點在於 clickTrackHandler/clickThumbHandler 這兩個函數,他們分別用於控制滾動條 container 被點擊時的行爲,以及滾動條自己被點擊的時候產生的行爲。
clickTrackHandler(e) { /** * 0. 以垂直滾動條爲例: * this.bar.direction = "top"/this.bar.client = "clientY"/this.bar.offset="offsetHeight"/this.bar.scrollSize="scrollHeight" * 1. getBoundingClientRect()[this.bar.direction] 返回元素的 top 值(距離瀏覽器視口的高度值) * 2. 用 1 的值減去 e.clientY(鼠標當前位置), 再用 Math.abs 得出相對值,這個值就是鼠標在滾動條 container 上的相對偏移量。 * 3. 計算出滾動條滑塊的一半位置 thumbHalf * 4. offset - thumbHalf 獲得具體偏移量,併除以整個 bar 的 offsetHeight,獲得了滑塊新的位置的百分比。 * 5. 接下來就能夠愉快地更新 wrap 元素的 scrollTop,顯示新的內容啦~ * 6. wrap 滾動後會觸發 handleScroll 方法,回過頭來更新 Bar 組件的 move 值,從而更新滾動條位置。 */ const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]); const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2); // 計算點擊後,根據 偏移量 計算在 滾動條區域的總高度 中的佔比,也就是 滾動塊 所處的位置 const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]); // 設置外殼的 scrollHeight 或 scrollWidth 新值。達到滾動內容的效果 this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100); },
這裏主要是計算拖動時滑塊的高度與整個滾動條的比例,從而更新 wrap 元素的 scrollTop 值,具體代碼與 clickTrackHandler 較爲類似,因爲篇幅所限,就不贅述了。
這裏有一個小點,咱們是給滑塊元素綁定 onMousedown 事件的,可是 mousemove 和 mouseup 倒是綁定在 document 上的,這是由於鼠標在移動過程當中,會比滑塊的移動要快,此時滑塊元素會失去 onMousemove 事件,因此綁定 mousemove 的時候不能綁定在對應元素上。
咱們從整個滾動條元素的生命週期,看到 element 是如何建立出一個滾動條,如何監聽元素的變化,如何控制滾動條的滑動等等。源碼的閱讀到這裏就所有結束了,若有什麼錯漏,請幫忙指出來;如你有所收穫,是我莫大的榮幸。
感謝:
Element-ui el-scrollbar 源碼解析
ResizeObserver API