天天看点

day-ui - Affix 组件学习

固钉组件是把页面某个元素相对页面

HTML

或者某个

dom

内定位显示,例如固定页面顶部/底部显示,页面宽高改变也会保持原位置。如果进行滚动,超过定义的范围就会固定定位,否则会跟随页面滚动
day-ui - Affix 组件学习
上一节我们介绍了

DButton

DIcon

的实现,所以新建

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 结构

<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名           
day-ui - Affix 组件学习
  • 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

组件的学习。如有不对欢迎指正。

继续阅读