Анализ исходного кода Element-ui el-scrollbar

Element

Несколько дней назад, когда я украшал свой блог, я обнаружил, что полоса прокрутки слишком некрасива под окном, поэтому я искал способ украсить полосу прокрутки на основе технологии vue. Помните, что в исходном коде Element-ui есть имяel-scrollbarКомпонент прокрутки, хотя и не упоминается в документации, до сих пор используется многими людьми. Запишите опыт чтения исходного кода сегодня.

перед этим

Прежде чем смотреть на горький код, сначала кратко опишите использование и поведение компонента полосы прокрутки, чтобы облегчить понимание логики кода.

Из-за разных операционных систем и браузеров внешний вид полосы прокрутки отличается. Когда вам нужно сделать единый стиль, вам нужно сделать пользовательскую полосу прокрутки. Конечно, вы также можете напрямую изменить CSS3.::-webkit-scrollbarСвязанные свойства для изменения внешнего вида собственной полосы прокрутки, но это свойство не полностью совместимо с некоторыми браузерами. Также трудно добиться интерактивных эффектов, таких как анимация.

В элементе с фиксированной высотой внутреннее содержимое превышает фиксированную высоту родительского элемента. Чтобы пользователь мог просматривать остальной контент, родительский элемент обычно устанавливаетсяoverflow-y: scrollПоявится полоса прокрутки. Позволяет пользователю прокручивать остальной контент.

Пользовательская полоса прокрутки должна сначала сместить элемент представления, чтобы добиться эффекта скрытия собственной полосы прокрутки. При этом справа и снизу от элемента представления добавьте имитационные полосы прокрутки, написанные метками. Прислушивайтесь к событиям, которые имитируют полосу прокрутки (нажатие ползунка или щелчок по дорожке), чтобы динамически обновлять окно просмотра.scrollTopилиscrollLeftстоимость. Точно так же он также прослушивает события окна просмотра (событие прокрутки или событие размера и масштабирования окна просмотра), чтобы синхронно обновить состояние пользовательской полосы прокрутки (положение ползунка или длина ползунка).

Полоса прокрутки на самом деле представляет собой интуитивно понятное отображение содержимого, просматриваемого в данный момент.scrollTopПрокрутите вниз для изменений. Ползунок на полосе прокрутки также перемещается вниз. В это время можно использовать полосу прокрутки, чтобы узнать степень прокрутки и оставшуюся степень содержимого.

Мы представляем страницу как длинный холст, и все, что мы можем видеть, — это движущееся окно. Когда страница прокручивается вниз, окно перемещается вниз по холсту, чтобы увидеть, что скрыто. Точно так же ползунок в блоке прокрутки перемещается вниз на такое же пропорциональное расстояние. Таким образом, полоса прокрутки представляет собой уменьшенную модель.

То есть высота неподвижного элементаclientHeightделенная на общую высоту неподвижных элементов, включая переливscrollHeight. Эквивалентно высоте ползунка, деленной на высоту полосы прокрутки. Их пропорции одинаковы.

未滚动前的滚动条
滚动后的滚动条

После примерного понимания рабочего содержимого и формулы расчета полосы прокрутки давайте посмотрим, как взаимосвязь расчета между ними обрабатывается в исходном коде.

документ

Компонент полосы прокрутки находится вpackage/scrollbar/index.jsэкспортируется, в которомpackage/scrollbar/srcявляется основной частью кода, входной файлmain.js.

структура

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

Используйте пользовательские ярлыкиel-scrollbarоберните используемую область,scrollbarкомпонент будет генерироватьviewа такжеwrapДва родительских элемента оборачивают содержимое в слот и два типа настраиваемых полос прокрутки.horizontalа такжеvertical.

生成后的结构

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          //  水平滚动条的移动比例
  };
},

Компоненты генерируют структуры в функции рендеринга.

Советы: если он существует в файле .vue одновременноtemplateа такжеrenderфункция, экземпляр компонента будет выбран первымtemplatetemplate для отображения шаблона компонента вместо использования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. Создайте внешний контейнер и запишите ширину смещения внешнего контейнера.
  2. Установите переполнение внешнего контейнера: прокрутите и создайте новый внутренний контейнер и добавьте его к внешнему контейнеру.
  3. В этот момент внешний контейнер будет иметь полосу прокрутки для записи ширины смещения внутреннего контейнера.
  4. Расчет ширины прокрутки ширина и возврат

用于计算滚动条宽度的临时标签结构
outer宽
inner宽
Таким образом, ширина полосы прокрутки браузера на данный момент составляет 100 - 83 = 17 пикселей.

Если есть ширина полосы прокрутки, настройка переноса будет смещена, чтобы добиться эффекта скрытия собственной полосы прокрутки.

//  如果存在滚动条宽度
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
const view = h(
  //  view的标签类型
  this.tag,
  //  view的属性
  {
    class: ["el-scrollbar__view", this.viewClass],
    style: this.viewStyle,
    ref: "resize"
  },
  //  接收的插槽内容
  this.$slots.default
);

Затем создайте структуру переноса, задайте настроенное имя стиля и встроенный стиль и прослушайте события прокрутки.

//  生成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>
);

Затем в соответствии с нативной конфигурацией производится сращивание окончательной структуры компонента.

//  如果不使用原生滚动条,则添加自定义滚动条
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函数结束

в компонентеmountedа такжеbeforeDestroyКогда событие перечислено на основе конфигурации.

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 завершен. Затем мы смотрим на обратный вызов прокрутки, связанный оберткойhandleScrollметоды, как показано в хуках жизненного циклаupdateметод.

Когда окно обертки прокручивается, оно будет выполненоmethodсерединаhandleScrollметод, обновлениеdataсерединаmoveYа такжеmoveXАтрибуты.moveYа такжеmoveXбудет передано как свойство конфигурации вBarКомпонент полосы прокрутки, обновление в реальном времениBarизtranslateY(moveY%)илиtranslateX(moveX%)как положение прокрутки ползунка.

handleScroll() {
  const wrap = this.wrap;

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

moveYа такжеmodeXЛогика вычисления , сначала выглядит немного запутанной. Но когда я изменил порядок вычислений, я вдруг понял.

handleScroll() {
  const wrap = this.wrap;

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

Вот отношение высоты прокрутки к видимой высоте. Как мы уже знаем выше, высота неподвижного элементаclientHeightделенная на общую высоту неподвижных элементов, включая переливscrollHeight. Эквивалентно высоте ползунка, деленной на высоту полосы прокрутки. Так когдаscrollTopКогда происходит изменение, мы можем рассчитать масштаб, чтобы обновить правильное положение ползунка.

Предположим, что наша высота обтекания составляет 300 пикселей, текущая высота прокруткиscrollTopравно 0, положение блока прокрутки близко к верху, в это времяBarкомпонентtranslateYсоставляет 0%.Обратите внимание, что полоса прокрутки справа и содержимое представления слева на самом деле не имеют одинаковой высоты. Просто масштабное соотношение.

当scrollTop为0时

Когда вы прокрутите вниз,scrollTopЭто ровно 300 пикселей (высота Wrap), и блок прокрутки сбоку также должен двигаться вниз всего на одну позицию. То есть высота самого блока прокрутки.

当scrollTop为300px时

Когда область обтекания прокручивается вниз ровно на высоту обтекания, блок прокрутки сбоку также смещается вниз на длину всего блока прокрутки. В настоящее времяBarкомпонентtranslateYДолжно быть 100%.

Установлена ​​формула расчета:scrollTop(300 пикселей)/scrollHeight(300 пикселей) * 100 = 100.

Здесь умножается на 100 т.к. сборка брусаtranslateYЗадайте свойство в процентах. При продолжении прокрутки внизscrollTopЭто уже 550px.По формуле позиция блока прокрутки 550/300*100 - translateY(183.333%). Смещение примерно на 1,8 длины самого блока прокрутки,Barотражатьwrapсерединаcontainerтекущее размещение.

当scrollTop为550px时,滚动块已经到了底部
updateметод отвечает за обновлениеBarДлина ползунка, вmountedВ крюке жизненного циклаnoresizeНастройте шаблон представления для выборочного отслеживания события изменения размера окна.При изменении размера окна содержимого он будет выполняться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методом вычисляется процентная высота блока прокрутки, а затем назначаетсяsizeHeightилиsizeWidth. Обновите ширину прокрутки или высоту панели.heightPercentageОн рассчитывается с высокой / полной высоты прокатки видимой области и рассчитана. Доля слайдера одинакова в ствол прокрутки.

this.sizeHeight = heightPercentage < 100 ? heightPercentage + "%" : "";

в вычисленияхsizeHeightКогда оценка больше 100, когда содержимое после изменения размера больше, чем высота прокрутки, это означает, что блок прокрутки не нужен. На данный момент вся логика в main.js завершена. Краткое описание того, что делает main.js.

  1. Получить параметры конфигурации.
  2. Создайте область, используемую структурами переноса и просмотра, в соответствии с конфигурацией и добавьте настраиваемую полосу прокрутки в соответствии с конфигурацией.
  3. Прослушиватель событий прокрутки для переноса и прослушиватель событий изменения содержимого окна для просмотра.
  4. Обновляет положение ползунка или длину ползунка компонента Bar при прокрутке или изменении окна.

Затем дошел до Bar.js, как обрабатывать обновление окна просмотра при нажатии на ползунок и дорожку.

Bar.js

Компонент Bar получает три свойстваvertical,size,move, и добавил коллекцию свойств текущего типа блока прокрутки к вычисляемому свойству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);
},

В методе много расчетов по формулам, в которых сложно разобраться какое-то время. На следующем рисунке представлена ​​схема каждой переменной, а затем мы разбираем ее по одной.

变量图示
В методе на первом этапе вычисляется значение ползунка.смещение (смещение). Формула расчета смещения в коде:Расстояние щелкнутого элемента от верхней части окна браузераминусРасстояние между позицией щелчка мыши и верхней частью окна браузера, а затем найти абсолютное значение результата.

щелкните элементНа самом деле это площадь орбиты, на самом деле формулу можно заменить таким образом, что будет проще для понимания.

Расстояние от позиции щелчка мыши от верхней части окна браузера.минусРасстояние области полосы прокрутки от верхней части окна браузера

Поскольку в зависимости от того, где используется компонент 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изscrollTopНазначение выполнено, и представление прокручивается до содержимого, которое необходимо обновить.


//  计算点击后,根据 偏移量 计算在 滚动条区域的总高度 中的占比,也就是 滚动块 所处的位置
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Получить как строку из вычисляемого свойстваY,this['Y']будет использоваться для последующих расчетов.this['Y']Формула расчета: высота ползунка минус (расстояние между положением ползунка, на которое нажали, и верхним краем окна страницы).clientYминус расстояние элемента ползунка от верхней части окна страницыRect.top)

变量图示

this.bar.axisПолучить из вычисляемого свойства, которое возвращает строку X или Y. но вBarкомпонентdata, не правильноthis['X']илиthis['Y']Эти два свойства объявлены. Причина в том, чтоBarКомпоненты бывают двух типов: вертикальные и горизонтальные. Поэтому автор решил не объявлять его в начале, а повесить динамически через последующие операции.XилиYАтрибуты.

Следует отметить, что это динамически добавляемое свойство не является адаптивным свойством. то есть не выполняется vuegetter/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мероприятие. запись метода будет судитьcursorDownа такжеthis.['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. Углубленные принципы реагирования