当前栏目
day-ui – Affix 组件学习
固钉组件是把页面某个元素相对页面 HTML
或者某个 dom
内定位显示,例如固定页面顶部/底部显示,页面宽高改变也会保持原位置。如果进行滚动,超过定义的范围就会固定定位,否则会跟随页面滚动
(福利推荐:阿里云、腾讯云、华为云服务器最新限时优惠活动,云服务器1核2G仅88元/年、2核4G仅698元/3年,点击这里立即抢购>>>)
上一节我们介绍了 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?userCode=wrvvs1rm 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
组件的学习。如有不对欢迎指正。
你还在原价购买阿里云、腾讯云、华为云、天翼云产品?那就亏大啦!现在申请成为四大品牌云厂商VIP用户,可以3折优惠价购买云服务器等云产品,并且可享四大云服务商产品终身VIP优惠价,还等什么?赶紧点击下面对应链接免费申请VIP客户吧:
相关文章
- 前端食堂技术周刊第 64 期:Node.js19、Interop 2022、SvelteKit1.0、2022 Web 性能回顾
- JavaScript刷LeetCode拿offer-位运算5
- 用javascript分类刷leetcode19.数组(图文视频讲解)5
- JavaScript刷LeetCode拿offer-二叉树层序遍历篇5
- 用javascript分类刷leetcode9.位运算(图文视频讲解)5
- 22道js输出顺序问题,你能做出几道5
- 前端高频react面试题整理5
- 社招前端react面试题整理5失败
- Angular forRoot 方法的使用场合介绍
- 前端二面经典vue面试题指南5
- 每日一题之Vue数据劫持原理是什么?5
- 百度前端经典vue面试题整理5
- 从零手写react-router
- 前端必会手写面试题合集5
- 高级前端常考手写面试题合集5
- 每日一题之Vue的异步更新实现原理是怎样的?5
- 由浅入深读透vue源码:diff算法
- 后端人眼中的Vue(三)
- 客服系统前端开发:JavaScript获取URL中的协议部分和域名部分【唯一客服】网页在线客服系统
- 客服系统前端开发:JavaScript删除对象数组中指定key value的对象【唯一客服】网页在线客服系统