固钉组件是把页面某个元素相对页面 HTML
或者某个 dom
内定位显示,例如固定页面顶部/底部显示,页面宽高改变也会保持原位置。如果进行滚动,超过定义的范围就会固定定位,否则会跟随页面滚动
上一节我们介绍了 DButton
和 DIcon
的实现,所以新建 affix
文件目录结构我们就不多介绍了。我们主要学习一下内部实现方式,本质就是位置定位,我们要看下用了哪些判断和第三方库,如果有哪里不对欢迎指正。
效果分析
- 第一种情况是没有设置容器,可以根据
position
位置设置固定定位,如果位置设置top
,那么当监听到页面滚动,如果当前元素的top
值小于设置的偏移量,设置fixed
定位(反之bottom
是比较bottom
值大于页面高度和偏移量的差值设置fixed
定位) - 第二种情况是设置容器,那么
top / bottom
的是只在容器内显示的,容器不在页面后,定位元素也就消失。如果设置的top
值,那么当当前元素top
值小于偏移量同时容器的bottom
大于0,元素fixed
定位(反之bottom
偏移需要计算页面高度和bottom
值得对比)。
最近学习了解到,fixed
定位默认是相对与窗口的,但是如果给父节点定义属性transform、filter、perspective,fixed
定位就会相对父集,大家感兴趣的话可以自行查看。
代码分析
dom 结构
<template>
<div ref="root" class="d-affix" :style="rootStyle">
<!-- 定位元素 滚动时监听 root 位置和页面可视区的关系设置 fixed,定位的时候设置样式-->
<div :class="{ 'd-affix--fixed': state.fixed }" :style="affixStyle">
<slot></slot>
</div>
</div>
</template>
外层定义 d-affix
类,高度和内部的元素相同,为了当内部元素 fixed
定位脱离文档流时,页面占位结构不变;同时需要对比 d-affix
的 top
和 bottom
值判断元素何时脱离文档,何时复位。
属性
props: {
// 定位元素的层级
zIndex: {
type: Number,
default: 100
},
// 在哪个容器内,没传就是视图
target: {
type: String,
default: ''
},
// 上下偏移量
offset: {
type: Number,
default: 0
},
// 距上边距下边距
position: {
type: String,
default: 'top'
}
},
// 对外暴露两个方法,监听滚动和 fixed 状态改变
emits: ['scroll', 'change'],
setUp 核心
// 定位元素属性
const state = reactive({
fixed: false,
height: 0, // height of target 滚动时获取赋值
width: 0, // width of target
scrollTop: 0, // scrollTop of documentElement
clientHeight: 0, // 窗口高度
transform: 0 // 元素在 target 中定位时 y 方向移动
})
// 计算属性,滚动时才能具体获取
// d-affix 类一直存在文档流中,只要宽高,滚动位置判断是否 fixed
const rootStyle = computed(() => {
return {
height: state.fixed ? `${state.height}px` : '',
width: state.fixed ? `${state.width}px` : ''
}
})
// 定位元素属性
const affixStyle = computed(() => {
if (!state.fixed) return
const offset = props.offset ? `${props.offset}px` : 0
const transform = state.transform
? `translateY(${state.transform}px)`
: ''
return {
height: `${state.height}px`,
width: `${state.width}px`,
top: props.position === 'top' ? offset : '',
bottom: props.position === 'bottom' ? offset : '',
transform: transform,
zIndex: props.zIndex
}
})
滚动时定位属性的判断:
const updateState = () => {
// 获取 d-affix 节点信息
const rootRect = root.value.getBoundingClientRect()
// 获取 target 节点的信息
const targetRect = target.value.getBoundingClientRect()
state.height = rootRect.height
state.width = rootRect.width
// 没有 target 取 html 的 scrollTOP(有 target 在 target 中滚动)
state.scrollTop =
scrollContainer.value === window
? document.documentElement.scrollTop
: scrollContainer.value.scrollTop
state.clientHeight = document.documentElement.clientHeight
// 设置上边距
if (props.position === 'top') {
if (props.target) {
// 定位元素在 target 元素中滑动距离,bottom 持续改变
const difference = targetRect.bottom - props.offset - state.height
// target 元素top在可视区外面,bottom在可视区进行定位
state.fixed = props.offset > rootRect.top && targetRect.bottom > 0
state.transform = difference < 0 ? difference : 0
} else {
// 以html为相对容器,页面滚动,固定定位(d-affix 在可视区外)
state.fixed = props.offset > rootRect.top
}
} else {
// 设置下边距
if (props.target) {
const difference =
state.clientHeight - targetRect.top - props.offset - state.height
state.fixed =
state.clientHeight - props.offset < rootRect.bottom &&
state.clientHeight > targetRect.top
state.transform = difference < 0 ? -difference : 0
} else {
// offset + bottom > 视图高度,元素进行定位
state.fixed = state.clientHeight - props.offset < rootRect.bottom
}
}
}
const onScroll = () => {
updateState()
emit('scroll', {
scrollTop: state.scrollTop,
fixed: state.fixed
})
}
watch(
() => state.fixed,
() => {
emit('change', state.fixed)
}
)
// 页面挂载的时候
onMounted(() => {
if (props.target) {
// 注意传的格式
target.value = document.querySelector(props.target)
if (!target.value) {
throw new Error(`target is not existed: ${props.target}`)
}
} else {
target.value = document.documentElement // html
}
// 下面我们分析辅助函数
scrollContainer.value = getScrollContainer(root.value)
// 函数式编程,on 改写的 addEventListener
on(scrollContainer.value, 'scroll', onScroll)
addResizeListener(root.value, updateState)
})
// 页面即将关闭取消监听移除
onBeforeMount(() => {
off(scrollContainer.value, 'scroll', onScroll)
removeResizeListener(root.value, updateState)
})
辅助函数
- on
// 函数式编程处理元素监听
export const on = function(element, event, handler, useCapture = false) {
if (element && event && handler) {
element.addEventListener(event, handler, useCapture)
}
}
- off
export const off = function(element, event, handler, useCapture = false) {
if (element && event && handler) {
element.removeEventListener(event, handler, useCapture)
}
}
- getScrollContainer
/**
* 获取滚动容器
* @param {*} el 滚动的容器
* @param {*} isVertical 竖直滚动还是水平滚动
* @returns
*/
export const getScrollContainer = (el, isVertical) => {
if (isServer) return
let parent = el
while (parent) {
// 都没有就是 window
if ([window, document, document.documentElement].includes(parent)) {
return window
}
// 容器是否可滚动
if (isScroll(parent, isVertical)) {
return parent
}
parent = parent.parentNode
}
return parent
}
- isSserver
export default typeof window === 'undefined'
- isScroll
/**
*
* @param {*} el
* @param {*} isVertical 是否垂直方向 overflow-y
* @returns
*/
export const isScroll = (el, isVertical) => {
if (isServer) return
const determineDirection = isVertical === null || isVertical === undefined
const overflow = determineDirection
? getStyle(el, 'overflow')
: isVertical
? getStyle(el, 'overflow-y')
: getStyle(el, 'overflow-x')
return overflow.match(/(scroll|auto)/)
}
- getStyle
// 获取元素的属性值
export const getStyle = function(element, styleName) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelize(styleName)
if (styleName === 'float') {
/**
* ie6~8下:style.styleFloat
FF/chrome 以及ie9以上:style.cssFloat
*/
styleName = 'cssFloat' // FF/chrome 以及ie9以上 float兼容性写法
}
try {
const style = element.style[styleName]
if (style) return style
// 获取window对象, firefox低版本3.6 才能使用getComputed方法,iframe pupup extension window === document.defaultView,否则指向错误
// https://www.cnblogs.com/yuan-shuai/p/4125511.html
const computed = document.defaultView.getComputedStyle(element, '')
return computed ? computed[styleName] : ''
} catch (e) {
return element.style[styleName]
}
}
resize-observer-polyfill 库
这个库是我第一次见到,如果不看源码都不知道的。觉得还是挺有意思的,这里做个简单介绍。
这个库主要作用是监听元素 size
改变。通常情况下我们监听大小改变只能使用 window.size
或者 window.orientationchange
(移动端屏幕横向纵向显示)。resize
事件会在 1s
内触发 60
次左右,所以很容易在改变窗口大小时候引发性能问题,所以当我们监听某个元素变化的时候就显得有些浪费。
ResizeObserver API
是新增的,在有些浏览器还存在兼容性,这个库可以很好的进行兼容。ResizeObserver
使用了观察者模式,当元素 size
发生改变时候触发(节点的出现隐藏也会触发)。
用法
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
console.log('大小位置', entry.contentRect)
console.log('监听的dom', entry.target)
})
})
// 监听的对象是body,可以改变浏览器窗口大小看打印效果
observer.observe(document.body)// dom节点,不是类名 id名
width
:指元素本身的宽度,不包含padding,border
值height
:指元素本身的高度,不包含padding,border
值top
:指padidng-top
的值left
:指padding-left
的值right
:指left + width
的值bottom
: 值top + height
的值
方法
ResizeObserver.disconnect()
取消所有元素的监听ResizeObserver.observe()
监听元素ResizeObserver.unobserve()
结束某个元素的监听
组件使用
我们在 onMounted
中对 root
元素监听。页面滚动时候要监听,元素大小改变也要监听
import ResizeObserver from 'resize-observer-polyfill'
import isServer from './isServer'
const resizeHandler = function(entries) {
for (const entry of entries) {
/**
* const {left, top, width, height} = entry.contentRect;
* 'Element:', entry.target
Element's size: ${ width }px x ${ height }px`
Element's paddings: ${ top }px ; ${ left }px`
*/
const listeners = entry.target.__resizeListeners__ || []
if (listeners.length) {
// 元素改变直接执行方法
listeners.forEach(fn => fn())
}
}
}
// 监听element元素size改变,执行fn
export const addResizeListener = function(element, fn) {
if (isServer || !element) return
if (!element.__resizeListeners__) {
element.__resizeListeners__ = []
/**
* https://github.com/que-etc/resize-observer-polyfill
*
*/
element.__ro__ = new ResizeObserver(resizeHandler)
// 观察的对象
element.__ro__.observe(element)
}
element.__resizeListeners__.push(fn)
}
// 退出移除监听
export const removeResizeListener = function(element, fn) {
if (!element || !element.__resizeListeners__) return
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1)
if (!element.__resizeListeners__.length) {
// 取消监听
element.__ro__.disconnect()
}
}
以上就是对 affix
组件的学习。如有不对欢迎指正。