perf(components): [el-scrollbar] prevent re-render when scrolling (#5670)

* perf(components): [el-scrollbar] prevent re-render when scrolling

* chore: improve code
This commit is contained in:
msidolphin 2022-01-27 17:10:44 +08:00 committed by GitHub
parent 8322bcf843
commit 0d8f08ab9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 263 additions and 199 deletions

View File

@ -7,4 +7,4 @@ export default ElScrollbar
export * from './src/util'
export * from './src/scrollbar'
export * from './src/bar'
export * from './src/thumb'

View File

@ -2,13 +2,25 @@ import { buildProps } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
export const barProps = buildProps({
vertical: Boolean,
size: String,
move: Number,
ratio: {
type: Number,
required: true,
always: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '',
},
height: {
type: String,
default: '',
},
ratioX: {
type: Number,
default: 1,
},
ratioY: {
type: Number,
default: 1,
},
always: Boolean,
} as const)
export type BarProps = ExtractPropTypes<typeof barProps>

View File

@ -1,193 +1,42 @@
<template>
<transition name="el-scrollbar-fade">
<div
v-show="always || visible"
ref="instance"
:class="['el-scrollbar__bar', 'is-' + bar.key]"
@mousedown="clickTrackHandler"
>
<div
ref="thumb"
class="el-scrollbar__thumb"
:style="thumbStyle"
@mousedown="clickThumbHandler"
></div>
</div>
</transition>
<thumb :move="moveX" :ratio="ratioX" :size="width" :always="always" />
<thumb
:move="moveY"
:ratio="ratioY"
:size="height"
vertical
:always="always"
/>
</template>
<script lang="ts">
import {
computed,
defineComponent,
inject,
onBeforeUnmount,
ref,
toRef,
} from 'vue'
import { useEventListener, isClient } from '@vueuse/core'
import { scrollbarContextKey } from '@element-plus/tokens'
import { throwError } from '@element-plus/utils/error'
import { BAR_MAP, renderThumbStyle } from './util'
import { defineComponent, ref } from 'vue'
import Thumb from './thumb.vue'
import { barProps } from './bar'
const COMPONENT_NAME = 'Bar'
export default defineComponent({
name: COMPONENT_NAME,
components: {
Thumb,
},
props: barProps,
setup(props) {
const scrollbar = inject(scrollbarContextKey)
if (!scrollbar)
throwError(COMPONENT_NAME, 'can not inject scrollbar context')
const moveX = ref(0)
const moveY = ref(0)
const GAP = 4 // top 2 + bottom 2 of bar instance
const instance = ref<HTMLDivElement>()
const thumb = ref<HTMLDivElement>()
const handleScroll = (wrap: HTMLDivElement) => {
if (wrap) {
const offsetHeight = wrap.offsetHeight - GAP
const offsetWidth = wrap.offsetWidth - GAP
const barStore = ref({})
const visible = ref(false)
let cursorDown = false
let cursorLeave = false
let onselectstartStore:
| ((this: GlobalEventHandlers, ev: Event) => any)
| null = isClient ? document.onselectstart : null
const bar = computed(
() => BAR_MAP[props.vertical ? 'vertical' : 'horizontal']
)
const thumbStyle = computed(() =>
renderThumbStyle({
size: props.size,
move: props.move,
bar: bar.value,
})
)
const offsetRatio = computed(
() =>
// offsetRatioX = original width of thumb / current width of thumb / ratioX
// offsetRatioY = original height of thumb / current height of thumb / ratioY
// instance height = wrap height - GAP
instance.value![bar.value.offset] ** 2 /
scrollbar.wrapElement![bar.value.scrollSize] /
props.ratio /
thumb.value![bar.value.offset]
)
const clickThumbHandler = (e: MouseEvent) => {
// prevent click event of middle and right button
e.stopPropagation()
if (e.ctrlKey || [1, 2].includes(e.button)) return
window.getSelection()?.removeAllRanges()
startDrag(e)
const el = e.currentTarget as HTMLDivElement
if (!el) return
barStore.value[bar.value.axis] =
el[bar.value.offset] -
(e[bar.value.client] - el.getBoundingClientRect()[bar.value.direction])
moveY.value = ((wrap.scrollTop * 100) / offsetHeight) * props.ratioY
moveX.value = ((wrap.scrollLeft * 100) / offsetWidth) * props.ratioX
}
}
const clickTrackHandler = (e: MouseEvent) => {
if (!thumb.value || !instance.value || !scrollbar.wrapElement) return
const offset = Math.abs(
(e.target as HTMLElement).getBoundingClientRect()[bar.value.direction] -
e[bar.value.client]
)
const thumbHalf = thumb.value[bar.value.offset] / 2
const thumbPositionPercentage =
((offset - thumbHalf) * 100 * offsetRatio.value) /
instance.value[bar.value.offset]
scrollbar.wrapElement[bar.value.scroll] =
(thumbPositionPercentage *
scrollbar.wrapElement[bar.value.scrollSize]) /
100
}
const startDrag = (e: MouseEvent) => {
e.stopImmediatePropagation()
cursorDown = true
document.addEventListener('mousemove', mouseMoveDocumentHandler)
document.addEventListener('mouseup', mouseUpDocumentHandler)
onselectstartStore = document.onselectstart
document.onselectstart = () => false
}
const mouseMoveDocumentHandler = (e: MouseEvent) => {
if (!instance.value || !thumb.value) return
if (cursorDown === false) return
const prevPage = barStore.value[bar.value.axis]
if (!prevPage) return
const offset =
(instance.value.getBoundingClientRect()[bar.value.direction] -
e[bar.value.client]) *
-1
const thumbClickPosition = thumb.value[bar.value.offset] - prevPage
const thumbPositionPercentage =
((offset - thumbClickPosition) * 100 * offsetRatio.value) /
instance.value[bar.value.offset]
scrollbar.wrapElement[bar.value.scroll] =
(thumbPositionPercentage *
scrollbar.wrapElement[bar.value.scrollSize]) /
100
}
const mouseUpDocumentHandler = () => {
cursorDown = false
barStore.value[bar.value.axis] = 0
document.removeEventListener('mousemove', mouseMoveDocumentHandler)
document.removeEventListener('mouseup', mouseUpDocumentHandler)
restoreOnselectstart()
if (cursorLeave) visible.value = false
}
const mouseMoveScrollbarHandler = () => {
cursorLeave = false
visible.value = !!props.size
}
const mouseLeaveScrollbarHandler = () => {
cursorLeave = true
visible.value = cursorDown
}
onBeforeUnmount(() => {
restoreOnselectstart()
document.removeEventListener('mouseup', mouseUpDocumentHandler)
})
const restoreOnselectstart = () => {
if (document.onselectstart !== onselectstartStore)
document.onselectstart = onselectstartStore
}
useEventListener(
toRef(scrollbar, 'scrollbarElement'),
'mousemove',
mouseMoveScrollbarHandler
)
useEventListener(
toRef(scrollbar, 'scrollbarElement'),
'mouseleave',
mouseLeaveScrollbarHandler
)
return {
instance,
thumb,
bar,
thumbStyle,
visible,
clickTrackHandler,
clickThumbHandler,
handleScroll,
moveX,
moveY,
}
},
})

View File

@ -20,14 +20,14 @@
</component>
</div>
<template v-if="!native">
<bar :move="moveX" :ratio="ratioX" :size="sizeWidth" :always="always" />
<bar
:move="moveY"
:ratio="ratioY"
:size="sizeHeight"
vertical
ref="barRef"
:height="sizeHeight"
:width="sizeWidth"
:always="always"
/>
:ratio-x="ratioX"
:ratio-y="ratioY"
></bar>
</template>
</div>
</template>
@ -69,6 +69,7 @@ export default defineComponent({
const sizeWidth = ref('0')
const sizeHeight = ref('0')
const barRef = ref()
const moveX = ref(0)
const moveY = ref(0)
const ratioY = ref(1)
@ -85,13 +86,7 @@ export default defineComponent({
const handleScroll = () => {
if (wrap$.value) {
const offsetHeight = wrap$.value.offsetHeight - GAP
const offsetWidth = wrap$.value.offsetWidth - GAP
moveY.value =
((wrap$.value.scrollTop * 100) / offsetHeight) * ratioY.value
moveX.value =
((wrap$.value.scrollLeft * 100) / offsetWidth) * ratioX.value
barRef.value?.handleScroll(wrap$.value)
emit('scroll', {
scrollTop: wrap$.value.scrollTop,
@ -170,7 +165,7 @@ export default defineComponent({
scrollbar$,
wrap$,
resize$,
barRef,
moveX,
moveY,
ratioX,

View File

@ -0,0 +1,14 @@
import { buildProps } from '@element-plus/utils/props'
import type { ExtractPropTypes } from 'vue'
export const thumbProps = buildProps({
vertical: Boolean,
size: String,
move: Number,
ratio: {
type: Number,
required: true,
},
always: Boolean,
} as const)
export type ThumbProps = ExtractPropTypes<typeof thumbProps>

View File

@ -0,0 +1,194 @@
<template>
<transition name="el-scrollbar-fade">
<div
v-show="always || visible"
ref="instance"
:class="['el-scrollbar__bar', 'is-' + bar.key]"
@mousedown="clickTrackHandler"
>
<div
ref="thumb"
class="el-scrollbar__thumb"
:style="thumbStyle"
@mousedown="clickThumbHandler"
></div>
</div>
</transition>
</template>
<script lang="ts">
import {
computed,
defineComponent,
inject,
onBeforeUnmount,
ref,
toRef,
} from 'vue'
import { useEventListener, isClient } from '@vueuse/core'
import { scrollbarContextKey } from '@element-plus/tokens'
import { throwError } from '@element-plus/utils/error'
import { BAR_MAP, renderThumbStyle } from './util'
import { thumbProps } from './thumb'
const COMPONENT_NAME = 'Thumb'
export default defineComponent({
name: COMPONENT_NAME,
props: thumbProps,
setup(props) {
const scrollbar = inject(scrollbarContextKey)
if (!scrollbar)
throwError(COMPONENT_NAME, 'can not inject scrollbar context')
const instance = ref<HTMLDivElement>()
const thumb = ref<HTMLDivElement>()
const thumbState = ref({})
const visible = ref(false)
let cursorDown = false
let cursorLeave = false
let originalOnSelectStart:
| ((this: GlobalEventHandlers, ev: Event) => any)
| null = isClient ? document.onselectstart : null
const bar = computed(
() => BAR_MAP[props.vertical ? 'vertical' : 'horizontal']
)
const thumbStyle = computed(() =>
renderThumbStyle({
size: props.size,
move: props.move,
bar: bar.value,
})
)
const offsetRatio = computed(
() =>
// offsetRatioX = original width of thumb / current width of thumb / ratioX
// offsetRatioY = original height of thumb / current height of thumb / ratioY
// instance height = wrap height - GAP
instance.value![bar.value.offset] ** 2 /
scrollbar.wrapElement![bar.value.scrollSize] /
props.ratio /
thumb.value![bar.value.offset]
)
const clickThumbHandler = (e: MouseEvent) => {
// prevent click event of middle and right button
e.stopPropagation()
if (e.ctrlKey || [1, 2].includes(e.button)) return
window.getSelection()?.removeAllRanges()
startDrag(e)
const el = e.currentTarget as HTMLDivElement
if (!el) return
thumbState.value[bar.value.axis] =
el[bar.value.offset] -
(e[bar.value.client] - el.getBoundingClientRect()[bar.value.direction])
}
const clickTrackHandler = (e: MouseEvent) => {
if (!thumb.value || !instance.value || !scrollbar.wrapElement) return
const offset = Math.abs(
(e.target as HTMLElement).getBoundingClientRect()[bar.value.direction] -
e[bar.value.client]
)
const thumbHalf = thumb.value[bar.value.offset] / 2
const thumbPositionPercentage =
((offset - thumbHalf) * 100 * offsetRatio.value) /
instance.value[bar.value.offset]
scrollbar.wrapElement[bar.value.scroll] =
(thumbPositionPercentage *
scrollbar.wrapElement[bar.value.scrollSize]) /
100
}
const startDrag = (e: MouseEvent) => {
e.stopImmediatePropagation()
cursorDown = true
document.addEventListener('mousemove', mouseMoveDocumentHandler)
document.addEventListener('mouseup', mouseUpDocumentHandler)
originalOnSelectStart = document.onselectstart
document.onselectstart = () => false
}
const mouseMoveDocumentHandler = (e: MouseEvent) => {
if (!instance.value || !thumb.value) return
if (cursorDown === false) return
const prevPage = thumbState.value[bar.value.axis]
if (!prevPage) return
const offset =
(instance.value.getBoundingClientRect()[bar.value.direction] -
e[bar.value.client]) *
-1
const thumbClickPosition = thumb.value[bar.value.offset] - prevPage
const thumbPositionPercentage =
((offset - thumbClickPosition) * 100 * offsetRatio.value) /
instance.value[bar.value.offset]
scrollbar.wrapElement[bar.value.scroll] =
(thumbPositionPercentage *
scrollbar.wrapElement[bar.value.scrollSize]) /
100
}
const mouseUpDocumentHandler = () => {
cursorDown = false
thumbState.value[bar.value.axis] = 0
document.removeEventListener('mousemove', mouseMoveDocumentHandler)
document.removeEventListener('mouseup', mouseUpDocumentHandler)
restoreOnselectstart()
if (cursorLeave) visible.value = false
}
const mouseMoveScrollbarHandler = () => {
cursorLeave = false
visible.value = !!props.size
}
const mouseLeaveScrollbarHandler = () => {
cursorLeave = true
visible.value = cursorDown
}
onBeforeUnmount(() => {
restoreOnselectstart()
document.removeEventListener('mouseup', mouseUpDocumentHandler)
})
const restoreOnselectstart = () => {
if (document.onselectstart !== originalOnSelectStart)
document.onselectstart = originalOnSelectStart
}
useEventListener(
toRef(scrollbar, 'scrollbarElement'),
'mousemove',
mouseMoveScrollbarHandler
)
useEventListener(
toRef(scrollbar, 'scrollbarElement'),
'mouseleave',
mouseLeaveScrollbarHandler
)
return {
instance,
thumb,
bar,
thumbStyle,
visible,
clickTrackHandler,
clickThumbHandler,
}
},
})
</script>