Element-ui el-scrollbar 源碼解析

前幾天美化博客時發現滾動條在window下實在太難看,因此在基於vue的技術上尋找美化滾動條的方法。記得Element-ui源碼中有名爲 el-scrollbar 的滾動組件,雖然文檔上沒有提到,但使用的人仍是很多。今天記錄下源碼的閱讀心得。html

在這以前

在看苦澀的代碼前,先大概描述一下滾動條組件的用處和行爲,方便理解代碼邏輯。vue

由於操做系統和瀏覽器的不一樣,滾動條外觀是不同的。須要作風格統一時,就須要作自定義滾動條。固然也能夠直接修改CSS3中的 ::-webkit-scrollbar 相關屬性來達到修改原生滾動條外觀,但這個屬性部分瀏覽器上沒有可以完美兼容。node

在一個固定高度的元素中,如內部內容超出了父級元素的固定高。爲了讓用戶瀏覽其他的內容,一般都會設置父級元素overflow-y: scroll 出現滾動條。容許用戶以滾動的形式來瀏覽剩下的內容。react

而自定義滾動條,是先經過偏移視圖元素,達到隱藏原生滾動條的效果。同時在視圖元素的右側和下方,增長用標籤寫出的模擬滾動條。監聽模擬滾動條的事件(按下滑塊或點擊軌道),來動態更新視圖窗口的scrollTopscrollLeft值。一樣的,也會監聽視圖窗口的事件(滾動事件或視圖窗口的尺寸縮放事件),來同步更新自定義滾動條的狀態(滑塊所處的位置或滑塊長度)。web

滾動條實際上是當前已瀏覽內容的一個直觀展現,在固定元素中,若是scrollTop發生改變往下滾動。滾動條中的滑塊也會向下移動。此時可以經過滾動條來得知內容的已滾動程度和剩餘程度。數組

咱們將頁面想象成一個很長的畫布,而咱們能看到的是一個移動的窗口。當頁面往下滾動時,窗口在畫布中也就往下移動,來查看被遮擋的內容。一樣的,滾動塊裏的滑塊也往下移動一樣比例的距離。因此滾動條就是一個等比例的縮小模型。瀏覽器

也就是說,固定元素的高度clientHeight 除以 固定元素包括溢出的總高度scrollHeight。同等於 滑塊的高度 除以 滾動條的高度。他們的比例是同樣的。閉包

未滾動前的滾動條
滾動後的滾動條

在大概瞭解滾動條的工做內容和計算公式後,看看源碼中是如何處理他們之間的計算關係的。app

文件

scrollbar組件在 package/scrollbar/index.js 中被導出,其中 package/scrollbar/src 是代碼的核心部分,入口文件是 main.jsdom

結構

<el-scrollbar>
  <div style="height: 300px;">
    <div style="height: 600px;"></div>
  </div>
</el-scrollbar>
複製代碼

使用自定義標籤 el-scrollbar 裹住使用的區域,scrollbar 組件會生成 viewwrap 兩個父級元素包裹插槽中的內容,還有兩種類型的自定義滾動條 horizontalvertical

生成後的結構

main.js

main.js默認導出一個對象,接收一系列配置。

name: 'ElScrollbar',

components: { 
  // 滾動條組件,擁有水平與垂直兩種形態
  Bar 
},

props: {
  native: Boolean,    // 是否使用原生滾動條,即不附加自定義滾動條
  wrapStyle: {},      // wrap的內聯樣式
  wrapClass: {},      // wrap的樣式名
  viewClass: {},      // view的樣式名
  viewStyle: {},      // view的內聯樣式
  noresize: Boolean,  // 當container尺寸發生變化時,自動更新滾動條組件的狀態
  tag: {              // 組件最外層的標籤屬性,默認爲 div
    type: String,
    default: 'div'
  }
},

data() {
  return {
    sizeWidth: '0',   // 水平滾動條的寬度
    sizeHeight: '0',  // 垂直滾動條的高度
    moveX: 0,         // 垂直滾動條的移動比例
    moveY: 0          // 水平滾動條的移動比例
  };
},
複製代碼

組件在render函數中生成結構。

tips:若是在.vue文件中同時存在 templaterender 函數,組件實例會先取 template 模板來渲染組件模板,而不採用 render函數

render函數一開始會經過 scrollbarWidth 方法來計算當前瀏覽器的滾動條寬度。

render(h) {
    // 獲取瀏覽器的滾動條寬度
    let gutter = scrollbarWidth();
    // wrap內聯樣式
    let style = this.wrapStyle;
    
    ...
複製代碼

scrollbarWidth 方法在 scrollbar-width.js 中被默認導出。

import Vue from 'vue';

// 閉包變量,用於記錄滾動條寬度
let scrollBarWidth;

export default function() {
  // 若是在服務端運行,返回 0
  if (Vue.prototype.$isServer) return 0;
  // 如存在滾動條寬度,直接返回
  if (scrollBarWidth !== undefined) return scrollBarWidth;

  // 建立outer標籤並隱藏
  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;
  // 設置外層div滾動屬性
  outer.style.overflow = 'scroll';
  // 建立inner標籤,並追加到outer標籤中
  const inner = document.createElement('div');
  inner.style.width = '100%';
  outer.appendChild(inner);
  // 此時outer已經能夠滾動,記錄下inner元素的寬度
  const widthWithScroll = inner.offsetWidth;
  // 銷燬outer元素
  outer.parentNode.removeChild(outer);
  // 滾動條寬度 = 沒有滾動條時的outer寬度 減去 有滾動條的outer中的inner寬度
  scrollBarWidth = widthNoScroll - widthWithScroll;
  // 返回滾動條寬度
  return scrollBarWidth;
};
複製代碼

獲取滾動條方法會進行如下步驟

  1. 建立outer容器,並記錄outer容器的offsetwidth
  2. 設置outer容器overflow: scroll,並新建inner容器,追加到outer容器下
  3. 此時outer容器會帶有滾動條,記錄inner容器的offsetwitdh寬度
  4. 計算滾動條寬度,並返回

用於計算滾動條寬度的臨時標籤結構
outer寬
inner寬
從而得出此時的瀏覽器滾動條寬度爲 100 - 83 = 17 像素

若是存在滾動條寬度,會將wrap設置偏移,達到隱藏原生滾動條的效果。

// 若是存在滾動條寬度
if (gutter) {
  // 設置偏移寬度,隱藏原生滾動條
  const gutterWith = `-${gutter}px`;
  const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`;
  
  // 根據配置類型,生成樣式
  /** * 如是對象數組屬性 Array<Object> [{"background": "red"}, {"color": "red"}] * 則會被轉爲對象 {background: "red", color: "red"} */
  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;
  }
}
複製代碼

接着生成view結構,設置配置的樣式名和內聯樣式,插槽中的默認內容會放入view下,同時給view增長ref索引,用於後續的事件綁定。

// 生成view
const view = h(
  // view的標籤類型
  this.tag,
  // view的屬性
  {
    class: ["el-scrollbar__view", this.viewClass],
    style: this.viewStyle,
    ref: "resize"
  },
  // 接收的插槽內容
  this.$slots.default
);
複製代碼

接着生成wrap結構,設置配置的樣式名和內聯樣式,同時監聽滾動事件

// 生成wrap,並監聽滾動事件
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 配置,拼接組件的最終結構。

// 若是不使用原生滾動條,則添加自定義滾動條
if (!this.native) {
  /** * 使用自定義滾動條 * <div class="el-scrollbar__wrap"> * <div class="el-scrollbar__view"></div> * </div> * <bar> * <bar> */
  nodes = [
    wrap,
    <Bar move={this.moveX} size={this.sizeWidth} />,
    <Bar vertical move={this.moveY} size={this.sizeHeight} />
  ];
} else {
  /** * 不然使用原生滾動條 * * <div class="el-scrollbar__wrap"> wrap並沒有監聽滾動事件 * <div class="el-scrollbar__view"></div> * </div> */
  nodes = [
    <div ref="wrap" class={[this.wrapClass, "el-scrollbar__wrap"]} style={style} > {[view]} </div>
  ];
}

// 返回最終結構
return h("div", { class: "el-scrollbar" }, nodes);
// render函數結束
複製代碼

在組件 mountedbeforeDestroy 時,根據配置進行事件監聽。

mounted() {
  // 如使用原生滾動條,返回
  if (this.native) return;
  // 在下一更新循環結束執行更新方法
  this.$nextTick(this.update);
  // 根據配置進行監聽內容窗口大小重置事件,執行更新方法
  !this.noresize && addResizeListener(this.$refs.resize, this.update);
},

beforeDestroy() {
  // 如使用原生滾動條,返回
  if (this.native) return;
  // 根據配置移除監聽內容窗口大小重置事件的執行更新方法
  !this.noresize && removeResizeListener(this.$refs.resize, this.update);
}
複製代碼

addResizeListener 方法在 resize-event.js 中被導出,方法接收兩個參數。監聽的DOM節點和回調事件。

/** * 窗口縮放執行回調 */
function resizeHandler(entries) {
  // entry是ResizeObserver構造函數執行時傳入的參
  for (let entry of entries) {
    // 取出以前聲明的回調函數數組
    const listeners = entry.target.__resizeListeners__ || [];
    // 遍歷執行回調
    if (listeners.length) {
      listeners.forEach(fn => {
        fn();
      });
    }
  }
}

/** * 添加尺寸改變時事件監聽 * @param {HTMLDivElement} element 元素 * @param {Function} fn 回調 */
const addResizeListener = function(element, fn) {
  if (!element.__resizeListeners__) {
    // 設置當前元素的事件回調數組
    element.__resizeListeners__ = [];
    // 實例化Resize觀察者對象
    element.__ro__ = new ResizeObserver(resizeHandler);
    // 開始觀察指定的目標元素,當元素尺寸改變時,會執行resizeHandler方法
    element.__ro__.observe(element);
    window.ro = element.__ro__;
  }
  // 往回調數組中添加本次監聽事件
  element.__resizeListeners__.push(fn);
};

/** * 移除尺寸改變時事件監聽 * @param {HTMLDivElement} element 元素 * @param {Function} fn 回調 */
const removeResizeListener = function(element, fn) {
  if (!element || !element.__resizeListeners__) return;
  // 數組中移除
  element.__resizeListeners__.splice(
    element.__resizeListeners__.indexOf(fn),
    1
  );
  // 取消目標對象上全部對element的觀察
  if (!element.__resizeListeners__.length) {
    element.__ro__.disconnect();
  }
};
複製代碼

這樣,main.js的實例化過程就結束了。接着咱們看wrap綁定的滾動回調handleScroll方法,和生命週期鉤子中見到的update方法。

在wrap窗口滾動時,會執行method中的handleScroll方法,更新data中的moveYmoveX屬性。 moveYmoveX會做爲配置屬性傳給Bar滾動條組件,實時更新BartranslateY(moveY%)translateX(moveX%)做爲滑塊的滾動位置。

handleScroll() {
  const wrap = this.wrap;

  this.moveY = (wrap.scrollTop * 100) / wrap.clientHeight;
  this.moveX = (wrap.scrollLeft * 100) / wrap.clientWidth;
},
複製代碼

moveYmodeX的計算邏輯,一開始看着有點迷糊。 可是調轉一下計算順序,就恍然大悟了。

handleScroll() {
  const wrap = this.wrap;

  this.moveY = (wrap.scrollTop / wrap.clientHeight) * 100;
  this.moveX = (wrap.scrollLeft / wrap.clientWidth) * 100;
},
複製代碼

這裏是在求滾動高度與可見高度的比例。 上面咱們已經知道,固定元素的高度clientHeight除以 固定元素包括溢出的總高度scrollHeight。同等於 滑塊的高度 除以 滾動條的高度。 因此當scrollTop發生改變時,咱們可以計算出比例關係來更新滑塊的正確位置。

假設咱們的wrap高度爲300px,當前的滾動高 scrollTop 爲0,滾動塊的位置是貼緊頂部的,此時Bar組件的 translateY是 0%。 注意,圖中右邊的滾動條和左側的視圖內容,並不真正同高。僅僅是比例尺關係。

當scrollTop爲0時

當向下滾動時,scrollTop恰好爲300px(一個Wrap的高度),側邊的滾動塊也應該往下移動恰好一個身位。也就是滾動塊的自身的高度。

當scrollTop爲300px時

當wrap區域往下滾動恰好一整個wrap的高度時,側邊的滾動塊也會往下移動一整個滾動塊的長度。此時Bar組件的 translateY應該是 100%。

計算公式成立:scrollTop(300px)/ scrollHeight(300px)* 100 = 100。

這裏乘100是由於Bar組件中 translateY 是以百分比爲單位設置屬性。 繼續滾動到底部時,此時的scrollTop已經爲550px,根據公式計算,550 / 300 * 100 滾動塊的位置爲 translateY(183.333%)。約要偏移1.8個滾動塊自身的長度,Bar才能反映出wrapcontainer的當前展現位置。

當scrollTop爲550px時,滾動塊已經到了底部
update 方法負責更新 Bar 的滑塊長度,在 mounted 生命週期鉤子中,會根據 noresize 配置對view模板進行選擇性監聽窗口大小改變事件,當內容窗口大小發生改變時,會執行 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方法中,會計算出滾動塊的百分比高度,而後賦值給sizeHeightsizeWidth。更新Bar的滾動塊寬度或高度。 heightPercentage是由 可見區域高度 / 總滾動高度,計算出的佔比。和滑塊在滾動條軌道中的佔比是同樣的。

this.sizeHeight = heightPercentage < 100 ? heightPercentage + "%" : "";
複製代碼

在計算sizeHeight時作了大於100判斷,當尺寸改變後的內容大於滾動高度,說明就不須要滾動塊了。 至此,main.js 中的全部邏輯都已通過完了。簡單總結一下 main.js 所作的事情。

  1. 接收配置參數。
  2. 根據配置生成wrap與view結構包裹使用的區域,根據配置添加自定義滾動條Bar。
  3. 對wrap進行滾動事件監聽,對view進行窗口內容改變事件監聽。
  4. 在滾動或窗口改變時,更新Bar組件的滑塊位置或滑塊長度。

而後來到Bar.js,在點擊滑塊和軌道時,如何處理視圖窗口的更新。

Bar.js

Bar組件接收三個屬性verticalsizemove,並在計算屬性中添加了當前滾動塊類型的屬性集合bar,與父組件的wrap索引。

export default {
    name: 'Bar',

    props: {
        // 是否垂直滾動條
        vertical: Boolean,
        // size 對應的是 水平滾動條的 width 或 垂直滾動條的height
        size: String,
        // move 用於 translateX 或 translateY 屬性中
        move: Number
    },

    computed: {
        /** * 從BAR_MAP中返回一個的新對象,垂直滾動條屬性集合 或 水平滾動條屬性集合 */
        bar() {
            return BAR_MAP[this.vertical ? 'vertical' : 'horizontal'];
        },
        // 父組件的wrap,用於鼠標拖動滑塊後更新 wrap 的 scrollTop 值
        wrap() {
            return this.$parent.wrap;
        }
    },
    ...
}
複製代碼

bar會返回當前滾動條類型的滾動條屬性集合,並在後續的操做中取對應的值做爲更新。

const BAR_MAP = {
    // 垂直滾動塊的屬性
    vertical: {
        offset: 'offsetHeight',
        scroll: 'scrollTop',
        scrollSize: 'scrollHeight',
        size: 'height',
        key: 'vertical',
        axis: 'Y',
        client: 'clientY',
        direction: 'top'
    },
    // 水平滾動塊的屬性
    horizontal: {
        offset: 'offsetWidth',
        scroll: 'scrollLeft',
        scrollSize: 'scrollWidth',
        size: 'width',
        key: 'horizontal',
        axis: 'X',
        client: 'clientX',
        direction: 'left'
    }
};
複製代碼

render函數中,會對軌道區域和滑塊進行鼠標按下事件進行監聽,並對滑塊進行內聯樣式綁定,在 size, move, bar 等屬性發生改變時,動態的改變滑塊的位置或長度。

render(h) {
    // size: 'width' || 'height'
    // move: 滾動塊的位置,單位爲百分比
    // bar: 垂直滾動條屬性集合 或 水平滾動條屬性集合
    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>
    );
}
複製代碼

咱們以垂直類型的Bar組件爲例,首先看綁定在軌道區域的鼠標點擊事件回調 clickTrackHandler 方法。 在點擊軌道區域時,滑塊會快速定位到該位置,而且更新視圖的scrollTop。這就是 clickTrackHandler 處理的事情。

// 對按下 滾動條區域 的某一個位置進行快速定位
clickTrackHandler(e) {
    /** * getBoundingClientRect() 方法返回元素的大小及其相對於瀏覽器頁面的位置。 * this.bar.direction = "top" * this.bar.client = "clientY" * e.clientY 是事件觸發時,鼠標指針相對於瀏覽器窗口頂部的距離。 */
    // 偏移量 絕對值 (當前元素距離瀏覽器窗口的 頂部/左側 距離 減去 當前點擊的位置距離瀏覽器窗口的 頂部/左側 距離)
    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);
},
複製代碼

方法中比較多的公式計算,一時之間比較難理解。下圖是各變量的圖示,接着咱們一個一個拆解。

變量圖示
在方法中,第一步會計算滑塊的 偏移量(offset)。代碼中的偏移量計算公式是: 點擊元素距離瀏覽器窗口頂部的距離 減去 鼠標點擊位置距離瀏覽器窗口頂部的距離,再求結果的絕對值。

點擊元素 實則就是軌道區域,其實公式能夠換成這樣看,會更加容易理解。

鼠標點擊位置距離瀏覽器窗口頂部的距離 減去 滾動條區域距離瀏覽器窗口頂部的距離

由於根據scrollBar組件的使用位置不一樣(有的包裹整個頁面窗口,有的包裹一小塊菜單區域),滾動條區域也不必定徹底貼緊瀏覽器窗口的頂部。因此這邊須要用 鼠標點擊位置距離瀏覽器窗口頂部的距離e[this.bar.client]滾動條區域距離瀏覽器窗口頂部的距離e.target.getBoundingClientRect()[this.bar.direction] 減去,才能得出準確的 偏移量offset

/** * getBoundingClientRect() 方法返回元素的大小及其相對於瀏覽器頁面的位置。 * this.bar.direction = "top" * this.bar.client = "clientY" * e.clientY 是事件觸發時,鼠標指針相對於瀏覽器窗口頂部的距離。 */

// 偏移量 絕對值 (當前元素距離瀏覽器窗口的 垂直/水平 座標 減去 當前點擊的位置距離瀏覽器窗口的 垂直/水平 座標)
const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]);

複製代碼

offset的計算
接下來計算的是滑動塊一半的高度,用於後續邏輯處理。

// 滑動塊一半高度
const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);

複製代碼

滾動塊一半高度計算
根據瀏覽器滾動條操做行爲,通常咱們點擊軌道某個點時,滑塊的中心總會在咱們的落點位置。 在用偏移量 offset 減去滾動塊的一半高度 thumbHalf 後得出 滑塊總移動的長度。再用 滑塊總移動的長度滾動區域的總高度,得出 滾動比例thumbPositionPercentage。 得出 滾動比例 後,由於滾動條和視圖是一個縮放的比例尺關係。此時用 滾動比例wrap的 scrollHeight 得出滾動距離,再對 wrapscrollTop 進行賦值,視圖便滾動到須要更新展現的內容中。

// 計算點擊後,根據 偏移量 計算在 滾動條區域的總高度 中的佔比,也就是 滾動塊 所處的位置
const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]);

// 設置外殼的 scrollTop 或 scrollLeft 新值。達到滾動內容的效果
this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);

複製代碼

計算wrap須要滾動的距離

接下來是滑塊監聽的鼠標按下事件,clickThumbHandler

clickThumbHandler 方法會在鼠標按下滑塊時,監聽鼠標移動事件和鼠標按鍵釋放事件,更新滑塊位置的同時,也更新視圖窗口的滾動位置。

// 按下滑動塊
clickThumbHandler(e) {
    /** * 防止右鍵單擊滑動塊 * e.ctrlKey: 檢測事件發生時Ctrl鍵是否被按住了 * e.button: 指示當事件被觸發時哪一個鼠標按鍵被點擊 0,鼠標左鍵;1,鼠標中鍵;2,鼠標右鍵 */
    if (e.ctrlKey || e.button === 2) {
        return;
    }
    // 開始記錄拖拽
    this.startDrag(e);
    // 記錄點擊滑塊時的位置距滾動塊底部的距離
    this[this.bar.axis] = (
        // 滑塊的高度
        e.currentTarget[this.bar.offset] - 
        // 點擊滑塊距離頂部的位置 減去 滑塊元素距離頂部的位置
        (e[this.bar.client] - e.currentTarget.getBoundingClientRect()[this.bar.direction])
    );
},
複製代碼

開始先判斷是否鼠標右鍵觸發的事件,如真返回。接着執行 startDrag 方法。 最後會計算點擊滑塊時的位置距滾動塊底部的距離。而後賦值給this[this.bar.axis],由於當前滾動條類型是垂直滾動條,因此this.bar.axis從計算屬性中獲取爲字符串 Ythis['Y']會用於後續的計算。 this['Y'] 的計算公式爲:滑塊的高度 減去 (點擊滑塊的位置距離頁面窗口頂部的距離 clientY 減去 滑塊元素距離頁面窗口頂部的距離Rect.top

變量圖示

this.bar.axis 從計算屬性中獲取,返回的是字符串,X 或 Y。但在Bar組件的 data 中,並無對 this['X']this['Y'] 這兩個屬性進行聲明。 緣由是由於Bar組件有兩種類型,垂直或水平。因此做者沒有選擇一開始就聲明,而是經過後續的操做再動態掛上 XY 屬性。

須要注意的是,這樣動態添加的屬性,並非一個響應式的屬性。即未被vue進行getter/setter重寫,在數據發生改變後視圖是不會同步更新的。 可是這裏僅僅用於數據層面上的使用,並不在視圖上使用。問題不大。 具體能夠查閱文檔, 深刻響應式原理

startDrag方法中,會記錄按下狀態,並監聽鼠標移動和鼠標按鈕鬆開事件。

// 開始拖拽
startDrag(e) {
    // 中止後續的相同事件函數執行
    e.stopImmediatePropagation();
    // 記錄按下狀態
    this.cursorDown = true;
    // 監聽鼠標移動事件
    on(document, 'mousemove', this.mouseMoveDocumentHandler);
    // 監聽鼠標按鍵鬆開事件
    on(document, 'mouseup', this.mouseUpDocumentHandler);
    // 拖拽滾動塊時,此時禁止鼠標長按劃過文本選中。
    document.onselectstart = () => false;
},
複製代碼

on方法和off方法在 utils/dom 中被導出,在導出時會對環境進行兼容處理,導出對應的事件監聽處理函數。

/* istanbul ignore next */
export const on = (function() {
  // 查詢實例是否在服務端運行,與是否支持 addEventListener,返回對應處理監聽函數
  if (!isServer && document.addEventListener) {
    return function(element, event, handler) {
      if (element && event && handler) {
        // 適用於現代瀏覽器的監聽事件 addEventListener
        element.addEventListener(event, handler, false);
      }
    };
  } else {
    return function(element, event, handler) {
      if (element && event && handler) {
        // 用於 ie 部分版本瀏覽器的監聽事件 attachEvent
        element.attachEvent('on' + event, handler);
      }
    };
  }
})();

/* istanbul ignore next */
export const off = (function() {
  // 查詢實例是否在服務端運行,與是否支持 removeEventListener,返回對應處理監聽函數
  if (!isServer && document.removeEventListener) {
    return function(element, event, handler) {
      if (element && event) {
        // 適用於現代瀏覽器的移除事件監聽 removeEventListener
        element.removeEventListener(event, handler, false);
      }
    };
  } else {
    return function(element, event, handler) {
      if (element && event) {
        // 用於 ie 部分版本瀏覽器的移除事件監聽 detachEvent
        element.detachEvent('on' + event, handler);
      }
    };
  }
})();
複製代碼

在鼠標移動時,會執行mouseMoveDocumentHandler事件。 方法進入會判斷cursorDownthis.['Y']是否存在,若是爲假。說明方法並非正常操做觸發,結束返回。 在鼠標的不斷移動中,計算按住滑塊移動時的位置距離軌道頂部的實際距離offset,同時用以前記錄下來的this['Y']計算出按下滑塊時距離滑塊頂部的距離thumbClickPosition。 此時offset減去thumbClickPosition,就是滑塊在軌道中實際移動的距離。再用此值除以軌道長度。即是滾動比例thumbPositionPercentage。 最後用thumbPositionPercentage乘視圖窗口的滾動高度,即是視圖窗口須要更新滾動的距離。

// 按下滾動條,而且鼠標移動時
mouseMoveDocumentHandler(e) {
   // 若是按下狀態爲假,返回
   if (this.cursorDown === false) return;
   // 點擊位置時距滾動塊底部的距離
   const prevPage = this[this.bar.axis];
   
   if (!prevPage) return;

   // (滑塊距離頁面頂部的距離 減 鼠標移動時距離頂部的距離) * -1
   const offset = ((this.$el.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]) * -1);
   
   // 按下滑塊位置距離滑塊頂部的距離
   const thumbClickPosition = (this.$refs.thumb[this.bar.offset] - prevPage);
   // 滑動距離在滾動軌道長度的佔比
   const thumbPositionPercentage = ((offset - thumbClickPosition) * 100 / this.$el[this.bar.offset]);
   // 根據比例,更新視圖窗口的滾動距離
   this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
},

複製代碼

mouseMoveDocumentHandler中的變量圖示
在鼠標鬆開時,重置各記錄的狀態,並取消監聽的鼠標移動事件。

// 按下滾動條,而且鼠標鬆開
mouseUpDocumentHandler(e) {
    // 重置按下狀態
    this.cursorDown = false;
    // 重置當前點擊在滾動塊的位置
    this[this.bar.axis] = 0;
    // 移除監聽鼠標移動事件
    off(document, 'mousemove', this.mouseMoveDocumentHandler);
    // 拖拽結束,此時容許鼠標長按劃過文本選中。
    document.onselectstart = null;
}
複製代碼

源碼到這裏已經所有解讀結束,因我的水平有限,不免會有不許確或者存在歧義的地方,但願可以不吝賜教,共同交流進步。 祝你有個愉快的勞動節假期。:)

Have a nice day.

參考資料

  1. ResizeObserver
  2. 深刻響應式原理
相關文章
相關標籤/搜索