Углубленный анализ исходного кода компонента element-ui ScrollBar
Корневой каталог компонента полосы прокрутки включает файл index.js и папку src.index.js — это место для регистрации подключаемого модуля Vue.плагин, содержимое каталога src является основным кодом компонента полосы прокрутки, а его входной файл — main.js.
Перед тем, как приступить к разбору исходного кода, поговорим о принципе работы пользовательских полос прокрутки, чтобы вы могли лучше понять.
Когда содержимое в обертке переполняется, будут созданы собственные полосы прокрутки каждого браузера.Чтобы реализовать собственные полосы прокрутки, мы должны удалить собственные полосы прокрутки. Предположим, мы обернули другой слой div снаружи обертки и установили стиль этого div наoverflow:hiddenВ то же время мы устанавливаем отрицательное значение для marginRight и marginBottom обтекания, и значение точно равно ширине собственной полосы прокрутки, В это время из-за свойства overflow:hidden родительского контейнера родную полосу прокрутки можно скрыть. Затем мы полностью позиционируем настраиваемую полосу прокрутки справа и снизу контейнера обертки и добавляем логику прокрутки, такую как события прокрутки и перетаскивания, для реализации пользовательской полосы прокрутки.
Далее мы начинаем с записи main.js и подробно анализируем, как элемент реализует эту логику.
Объект напрямую экспортируется из файла main.js. Этот объект использует функцию рендеринга для рендеринга компонента полосы прокрутки. Доступный интерфейс компонента выглядит следующим образом:
props: {
native: Boolean, // 是否采用原生滚动(即只是隐藏掉了原生滚动条,但并没有使用自定义的滚动条)
wrapStyle: {}, // 内联方式 自定义wrap容器的样式
wrapClass: {}, // 类名方式 自定义wrap容器的样式
viewClass: {}, // 内联方式 自定义view容器的样式
viewStyle: {}, // 类名方式 自定义view容器的样式
noresize: Boolean, // 如果 container 尺寸不会发生变化,最好设置它可以优化性能
tag: { // view容器用那种标签渲染,默认为div
type: String,
default: 'div'
}
}
Как видите, это интерфейс, предоставляемый всем компонентом ScrollBar, в основном включая интерфейс для пользовательских стилей обтекания и представления, а также интерфейс noresize для оптимизации производительности.
Затем давайте снова проанализируем функцию рендеринга:
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;
}
}
...
}
Самое важное в этом фрагменте кода — способ получить родную ширину полосы прокрутки браузера, для этого элемента специально определен метод 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;
};
На самом деле, это очень просто: динамически создать внешний подэлемент тела, задать фиксированную ширину 100 пикселей и установить переполнение для прокрутки, чтобы обтекание генерировало полосу прокрутки. вложенный элемент внутри внешнего и установите его ширину. Установите на 100%. Поскольку у внешнего есть полоса прокрутки, ширина внутреннего не должна быть равна ширине внешнего.В это время ширина внешнего вычитается из ширины внутреннего, а ширина полосы прокрутки браузера получается. Это тоже очень просто, и, наконец, не забудьте уничтожить динамически созданный внешний элемент из тела.
Давайте вернемся и посмотрим на функцию рендеринга.После динамического создания переменной стиля style в соответствии с шириной полосы прокрутки браузера и wrapStyle, следующим шагом будет создание HTML-кода компонента ScrollBar в функции рендеринга.
// 生成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>
);
Затем обертка собирается в соответствии с нативом, и представление генерирует все дерево узлов 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, используется пользовательская полоса прокрутки, а если это правда, пользовательская полоса прокрутки не используется. 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>
Для самой внешней полосы прокрутки установлено значение overflow: hidden, чтобы скрыть собственную полосу прокрутки браузера, сгенерированную в обертке. При использовании компонента ScrollBar содержимое, записанное в компоненте ScrollBar, будет распределяться внутри представления через слот. Кроме того, три интерфейса перемещения, размера и вертикали используются для вызова компонента Bar, который на схеме является Track и Thumb. Давайте взглянем на компонент Bar:
props: {
vertical: Boolean, // 当前Bar组件是否为垂直滚动条
size: String, // 百分数,当前Bar组件的thumb长度 / track长度的百分比
move: Number // 滚动条向下/向右发生transform: translate的值
},
Поведение компонента Bar контролируется этими тремя интерфейсами.В предыдущем анализе мы видели, что когда компонент Bar вызывается в полосе прокрутки, эти три реквизита передаются соответственно. Так как же родительский компонент инициализирует и обновляет значения этих трех параметров, чтобы обновить компонент Bar. Сначала вызовите метод обновления в смонтированном хуке, чтобы инициализировать размер:
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 + '%') : '';
}
Как видите, основное содержание здесь — вычисление длины большого пальца, heightPercentage/widthPercentage. Здесь используйте wrap.clientHeight / wrap.scrollHeight, чтобы получить процент длины большого пальца. Почему это
Анализируя схематическую диаграмму полосы прокрутки, которую мы нарисовали ранее, бегунок прокручивается вверх и вниз по дорожке, а вид прокручиваемой области прокручивается вверх и вниз в обтекании видимой области.Относительное отношение между бегунком и дорожкой можно рассматривать как относительное отношение между оберткой и просмотром одного изминиатюры(миниатюрный ответ), а значение полосы прокрутки используется для отражения относительной связи движения между представлением и переносом. С другой точки зрения, мы можем превратить прокрутку представления в обтекании в прокрутку вверх и вниз обтекания в представлении.Разве это не увеличенная полоса прокрутки?
Из этого сходства мы можем вывести пропорциональную зависимость: wrap.clientHeight / wrap.scrollHeight = thumb.clientHeight / track.clientHeight. Здесь нам не нужно находить конкретное значение thumb.clientHeight, нам нужно только установить процент css-высоты большого пальца в соответствии с соотношением thumb.clientHeight / track.clientHeight.
Еще один момент, на который стоит обратить внимание, это то, что когда соотношение больше или равно 100%, то есть когда wrap.clientHeight (высота контейнера) больше или равно wrap.scrollHeight (высота прокрутки), полосы прокрутки в этом случае не нужны. время, поэтому установите размер пустой строки.
Далее, давайте взглянем на перемещение, которое является обновлением положения прокрутки полосы прокрутки.
handleScroll() {
const wrap = this.wrap;
this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth);
}
moveX/moveY используется для управления положением прокрутки полосы прокрутки.Когда это значение передается компоненту Bar, будет вызываться функция рендеринга компонента Bar.renderThumbStyleметод, чтобы преобразовать его в стиль trumbtransform: translateX(${moveX}%) / transform: translateY(${moveY}%). Из отношения подобия, проанализированного ранее, когда wrap.scrollTop точно равен wrap.clientHeight, бегунок должен прокручиваться вниз на расстояние, равное его собственной длине, то есть transform: translateY(100%). Таким образом, при прокрутке прокрутки расстояние, на которое должен прокручиваться большой палец, точно равно преобразованию: translateY(wrap.scrollTop / wrap.clientHeight ). Это логика функции прокрутки обтекания handleScroll.
Теперь, когда мы полностью разобрались со всей логикой в компоненте полосы прокрутки, давайте посмотрим, как работает компонент Bar после получения реквизита.
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>
);
}
Функция рендеринга получает размер, переданный родительским компонентом, и после перемещения передаетrenderThumbStyleдля создания большого пальца и привязки события 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;
}
Приведенный выше код представляет собой всю логику обработки перетаскивания полосы прокрутки большого пальца Общая идея состоит в том, чтобы динамически вычислять расстояние от верхней (левой стороны) бегунка до верхней (левой стороны) дорожки в процессе перетаскивания бегунка. , который учитывает высоту (ширину) самой дорожки в процентах, а затем использовать этот процент для динамического изменения значения wrap.scrollTop, запуская тем самым прокрутку страницы и перерасчет положения полосы прокрутки для достижения эффекта прокрутки.
Предыдущая картинка удобна для понимания всем ( ̄▽ ̄)"
Логика треков onMousedown и trumb аналогична, стоит отметить два момента:
- Обратный вызов события onMousedown для отслеживания не будет привязывать события mousemove и mouseup к странице, поскольку отслеживание эквивалентно событию щелчка.
- В событии onmousedown дорожки способ, которым мы вычисляем верхнюю часть бегунка до начала дорожки, состоит в том, чтобы вычесть половину высоты бегунка из расстояния от точки щелчка мыши до верхней части дорожки, потому что после щелчка по треку середина бегунка находится точно в точке щелчка мыши.
На этом анализ всего исходного кода полосы прокрутки закончен.Оглядываясь назад, можно сказать, что реализация полосы прокрутки не сложна.Главное — разобраться в различных соотношениях прокрутки, длине бегунка и способе положения прокрутки. определяется отношением между оберткой и представлением. Эта часть может быть довольно запутанной. Студенты, которые этого не понимают, предлагают вам нарисовать и изучить ее самостоятельно. Пока вы понимаете этот принцип скользящего движения, его очень просто реализовать.