前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[day-ui] Affix 组件学习

[day-ui] Affix 组件学习

原创
作者头像
测不准
发布2021-05-26 13:01:44
1.2K0
发布2021-05-26 13:01:44
举报
文章被收录于专栏:与前端沾边与前端沾边

固钉组件是把页面某个元素相对页面 HTML 或者某个 dom 内定位显示,例如固定页面顶部/底部显示,页面宽高改变也会保持原位置。如果进行滚动,超过定义的范围就会固定定位,否则会跟随页面滚动

上一节我们介绍了 DButtonDIcon 的实现,所以新建 affix 文件目录结构我们就不多介绍了。我们主要学习一下内部实现方式,本质就是位置定位,我们要看下用了哪些判断和第三方库,如果有哪里不对欢迎指正。

效果分析

  1. 第一种情况是没有设置容器,可以根据 position 位置设置固定定位,如果位置设置 top,那么当监听到页面滚动,如果当前元素的 top 值小于设置的偏移量,设置 fixed 定位(反之 bottom 是比较 bottom 值大于页面高度和偏移量的差值设置 fixed 定位)
  2. 第二种情况是设置容器,那么 top / bottom 的是只在容器内显示的,容器不在页面后,定位元素也就消失。如果设置的 top 值,那么当当前元素 top 值小于偏移量同时容器的 bottom 大于0,元素 fixed 定位(反之 bottom 偏移需要计算页面高度和 bottom 值得对比)。 最近学习了解到fixed 定位默认是相对与窗口的,但是如果给父节点定义属性 transform、filter、perspective,fixed 定位就会相对父集,大家感兴趣的话可以自行查看。

代码分析

dom 结构

代码语言:txt
复制
<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-affixtopbottom 值判断元素何时脱离文档,何时复位。

属性

代码语言:txt
复制
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 核心

代码语言:txt
复制
// 定位元素属性
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
  }
})

滚动时定位属性的判断:

代码语言:txt
复制
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
    }
  }
}
代码语言:txt
复制
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) } }export const off = function(element, event, handler, useCapture = false) { if (element && event && handler) { element.removeEventListener(event, handler, useCapture) } }/** * 获取滚动容器 * @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 }export default typeof window === 'undefined'/** * * @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)/) }// 获取元素的属性值 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 库这个库是我第一次见到,如果不看源码都不知道的。觉得还是挺有意思的,这里做个简单介绍。
  • off
  • getScrollContainer
  • isSserver
  • isScroll
  • getStyle

这个库主要作用是监听元素 size 改变。通常情况下我们监听大小改变只能使用 window.size 或者 window.orientationchange(移动端屏幕横向纵向显示)。resize 事件会在 1s内触发 60 次左右,所以很容易在改变窗口大小时候引发性能问题,所以当我们监听某个元素变化的时候就显得有些浪费。

ResizeObserver API 是新增的,在有些浏览器还存在兼容性,这个库可以很好的进行兼容。ResizeObserver 使用了观察者模式,当元素 size 发生改变时候触发(节点的出现隐藏也会触发)。

用法

代码语言:txt
复制
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 元素监听。页面滚动时候要监听,元素大小改变也要监听

代码语言:txt
复制
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 组件的学习。如有不对欢迎指正。下一篇我们进行 alert 组件的学习。如果文章对您有帮助,欢迎关注公众号 与前端沾边,或者加小编微信:wajh123654789,一起学习。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 效果分析
  • 代码分析
    • dom 结构
      • 属性
        • setUp 核心
          • 辅助函数
            • 用法
              • 方法
                • 组件使用
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档