zoukankan      html  css  js  c++  java
  • 无限滚动&懒加载&元素位置 etc....

    最近遇到了无限滚动的问题,用到了IntersectionObserver API,这里正好就将相关知识梳理整理一下。

    前置概念知识

    1. scrollHeight:容器元素被撑起后的实际大小高度
    2. scrollTop:容器元素的滚动距离,也是当前视口相对于容器实际高度定点的距离。
    3. offsetTop:元素的上外边框至包含元素的上内边框之间的像素距离。
    4. el.getBoundingClientRect().top:方法返回元素的大小及其相对于视口的位置。(top相对于视口的高度距离)
    5. clientHeight:元素内容区高度加上上下内边距高度,clientHeight = content + padding。
    6. IntersectionObserver API:
      (阮老师的这篇文章介绍的很清晰:连接,下面是部分引用)
    var io = new IntersectionObserver(callback, option);
    // 开始观察
    io.observe(document.getElementById('example'));
    
    // 停止观察
    io.unobserve(element);
    
    // 关闭观察器
    io.disconnect();
    

    由浏览器原生提供的构造函数,异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

    构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。可观察多个实例,

    目标元素的可见性变化时,就会调用观察器的回调函数callback。
    callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

    var io = new IntersectionObserver(
      entries => {
        console.log(entries);
      }
    );
    

    回调函数的参数是一个IntersectionObserverEntry对象数组,监听了多少,数组就有多少IntersectionObserverEntry对象。
    对象包含了以下数据:
    在这里插入图片描述
    参数意思为:

    time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
    target:被观察的目标元素,是一个 DOM 节点对象
    rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
    boundingClientRect:目标元素的矩形区域的信息
    intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
    intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

    无限滚动实现方案

    1. 判断容器是否滚动到“底”

    一般底部有个提示性的footer,暂定20px的话,核心代码即为:

     container.scrollHeight -
            container.scrollTop -
            container.clientHeight <
            20
    

    注意一般会加上节流防抖,不同的浏览器的onscroll触发次数可能差别很大。判断到底后,就发请求append。

    完整代码实例:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Document</title>
      <style>
        body {
          margin: 0;
        }
    
        .container {
          height: 500px;
          overflow-y: scroll;
        }
    
        .item {
           100%;
          height: 200px;
          margin-bottom: 20px;
          background-color: antiquewhite;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      </style>
    </head>
    
    <body>
      <div class="container">
        <div class="item">1</div>
        <div class="item">2</div>
        <div class="item cur">3</div>
        <div class="item">4</div>
      </div>
      <script type="text/javascript">
        let container = document.getElementsByClassName("container")[0];
    
        function debounce(fn, delay) {
          let timer = null;
          return function (args) {
            if (timer) {
              clearTimeout(timer);
            }
            timer = setTimeout(function () {
              fn.apply(this, args);
            }, delay);
          };
        }
        let k = 5;
        const bottomAppend4item = () => {
          console.log('滚动函数执行了');
          // 20一般为滚动到底部前的一个底部标签大小值,这里以刚好的margin为例
          if (
            container.scrollHeight -
            container.scrollTop -
            container.clientHeight <
            20
          ) {
            console.log('true');
    
            let cnt = 4;
            while (cnt--) {
              console.log(1);
              let newItem = document.createElement("div");
              newItem.setAttribute("class", "item");
              newItem.innerText = `${k++}`
              container.appendChild(newItem)
            }
            cnt = 4;
    
          }
        }
        const debounceAJAX = debounce(bottomAppend4item, 50)
        container.onscroll = debounceAJAX;
      </script>
    </body>
    
    </html>
    

    2. IntersectionObserver检测最后一个元素的intersectionRatio显示比

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>Title</title>
        <style>
          .img-area {
             500px;
            height: 500px;
            margin: 0 auto;
          }
    
          .my-photo {
             500px;
            height: 300px;
          }
        </style>
      </head>
    
      <body>
        <div id="container">
          <div class="img-area">
            <img class="my-photo" alt="loading" src="./img/1.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" src="./img/2.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" src="./img/3.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" src="./img/4.png" />
          </div>
        </div>
        <div class="scrollerFooter1">
          没有内容了
        </div>
        <script>
          function infinityScroll(footerNode, callback) {
            var observer = new IntersectionObserver(function (changes) {
              // 注意intersectionRatio这个属性值的判断
              if (changes[0].intersectionRatio <= 0) return;
    
              callback();
            });
            observer.observe(document.querySelector(footerNode));
          }
          infinityScroll(".scrollerFooter1", function () {
            for (var i = 0; i < 3; i++) {
              document
                .getElementById("container")
                .appendChild(document.getElementById("container").firstChild);
            }
          });
        </script>
      </body>
    </html>
    
    

    用IntersectionObserver实现懒加载方案

    事先把img元素的链接放在data-*的data-src里,判断元素的intersectionRatio占比出现后,取出data-src动态赋值给src加载图片。

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>Title</title>
        <style>
          .img-area {
             500px;
            height: 500px;
            margin: 0 auto;
          }
    
          .my-photo {
             500px;
            height: 300px;
          }
        </style>
      </head>
    
      <body>
        <div class="container">
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/1.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/2.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/3.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/4.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/5.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/1.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/2.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/3.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/4.png" />
          </div>
          <div class="img-area">
            <img class="my-photo" alt="loading" data-src="./img/5.png" />
          </div>
        </div>
        <script>
          function lazyLoad(imgClassName) {
            const imgList = Array.from(document.querySelectorAll(imgClassName));
            var io = new IntersectionObserver(function (ioes) {
              ioes.forEach(function (ioe) {
                var el = ioe.target;
                var intersectionRatio = ioe.intersectionRatio;
                if (intersectionRatio > 0 && intersectionRatio <= 1) {
                  if (!el.src) {
                    el.src = el.dataset.src;
                  }
                }
              });
            });
            imgList.forEach(function (item) {
              io.observe(item);
            });
          }
          lazyLoad(".my-photo");
        </script>
      </body>
    </html>
    
    

    判断元素是否在视口中

    上面两个场景问题其实都可以引申出一个方法,如何判断元素出现在视口中。

    1. 滚动属性判断

    公式: el.offsetTop - document.documentElement.scrollTop <= viewPortHeight

    function isInViewPortOfOne (el) {
        // viewPortHeight 兼容所有浏览器写法
        const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
        const offsetTop = el.offsetTop
        const scrollTop = document.documentElement.scrollTop
        const top = offsetTop - scrollTop
        console.log('top', top)
         // 这里有个+100是为了提前加载+ 100
        return top <= viewPortHeight + 100
    }
    
    2. 直接getBoundingClientRect

    上面在前置中就介绍了这个方法就是相对于视口的距离,el.getBoundingClientReact().top <= viewPortHeight
    跟上面公式连在一起,可以得出:el.offsetTop - document.documentElement.scrollTop = el.getBoundingClientRect().top

    例子:

    function isInViewPortOfTwo (el) {
        const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
        const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
        console.log('top', top)
        return top  <= viewPortHeight + 100
    }
    
    3. IntersectionObserver的intersectionRatio比

    同上无限滚动和懒加载的代码和介绍

    看到有人问如果上面公式的结果小于0的怎么办不判断吗,小于0其实就是滚动离开了视口区域。我们一般利用这些方法对元素做初次的渲染,这个问题不在这个场景的考虑范畴。

    一些polyfill

    对于getBoundingClientRect的polyfill支持,可以用更“原始”的offsetXX属性实现。

    function getElementTop(element){
        var actualTop = element.offsetTop;
        var current = element.offsetParent;
        while (current !== null){
            actualTop += current. offsetTop;
            current = current.offsetParent;
        }
        return actualTop;
    }
    
    function getBoundingClientRect(element) {
        var scrollTop = document.documentElement.scrollTop;
        var scrollLeft = document.documentElement.scrollLeft;
        if (element.getBoundingClientRect) {
            if (typeof arguments.callee.offset != "number") {
                var temp = document.createElement("div");
                temp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp);
                arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop; document.body.removeChild(temp);
                temp = null;
            }
            var rect = element.getBoundingClientRect();
            var offset = arguments.callee.offset;
            return {
                left: rect.left + offset,
                right: rect.right + offset,
                top: rect.top + offset,
                bottom: rect.bottom + offset
            };
        } else {
            var actualLeft = getElementLeft(element);
            var actualTop = getElementTop(element);
            return {
                left: actualLeft - scrollLeft,
                right: actualLeft + element.offsetWidth - scrollLeft,
                top: actualTop - scrollTop,
                bottom: actualTop + element.offsetHeight - scrollTop
            }
        }
    }
    

    封装一个动态获取高度的组件

    之前一些中台项目,我们也封装了动态获取高度的React组件,用此高阶组件封装的组件,可以动态给子组件设置当前浏览器文档流剩余空间的高度,使整体刚好占满屏幕(当然超过有滚动条)。原理就是差不多也是用了上面的一些元素位置属性。

    import React, { useState, useRef, useLayoutEffect } from 'react';
    
    import { getElementTop, triggerEvent } from 'utils/tools';
    
    type ObtainHeightProps = {
      bgColor?: string; // 背景色
      children: React.ReactNode;
      overflow?: string;
      className?: string;
    };
    
    //  计算滚动内容高度的组件
    const ObtainHeight = (props: ObtainHeightProps) => {
      const { className, bgColor, overflow } = props;
      const obtainRef = useRef<HTMLDivElement>(null);
      const [height, setHeight] = useState(
        document.documentElement.clientHeight - 200
      );
    
      const setObtainHeight = () => {
        const contentDom: HTMLDivElement | null = obtainRef.current;
        const top = getElementTop(contentDom as HTMLDivElement);
        const clientHeight = document.documentElement.clientHeight - top - 20;
        setHeight(clientHeight);
      };
      useLayoutEffect(() => {
        setObtainHeight();
        window.onresize = () => {
          setObtainHeight();
        };
        // 主动触发一次resize,解决计算不准确的bug
        const evt = window.document.createEvent('UIEvents');
        evt.initEvent('resize', true, false);
        window.dispatchEvent(evt);
        setTimeout(() => {
          triggerEvent(window, 'resize');
        }, 200);
        return () => {
          window.onresize = null;
        };
      }, []);
    
      return (
        <div
          className={`${className} obtain-height`}
          ref={obtainRef}
          style={{ height, background: bgColor, overflow }}
        >
          {props.children}
        </div>
      );
    };
    export default ObtainHeight;
    
    
    /**
     * @description: 获取元素在页面距离顶部的偏移量
     * @param {element} 元素节点
     * @return:  Number
     */
    export const getElementTop: (element: HTMLDivElement) => number = (
      element: HTMLDivElement
    ) => {
      // 获取 element 元素距离父元素的 offsetTop 像素;
      // console.log(element);
      if (!element) return 200;
      let actualTop = element.offsetTop;
      let current = element.offsetParent;
      // console.log(element, actualTop, current);
      // 判断当前元素是都循环到 HTML 根元素了
      while (current !== null) {
        // offsetTop 循环相加
        actualTop += (current as HTMLDivElement).offsetTop;
        // 当 current 为 HTML 根元素是, current.offsetParent 的值为 null
        current = (current as HTMLDivElement).offsetParent;
        // console.log(element, actualTop, current);
      }
      // console.log(actualTop);
      return actualTop;
    };
    
    // 事件触发函数
    export function triggerEvent(el: Element | Window, type: string) {
      if ('createEvent' in document) {
        // modern browsers, IE9+
        const e = document.createEvent('HTMLEvents');
        e.initEvent(type, false, true);
        el.dispatchEvent(e);
      }
    }
    
  • 相关阅读:
    SpringBoot集成Redis
    独享锁 & 共享锁
    公平锁与非公平锁
    如何上传本地代码到码云
    SpringBoot+Mybatis+Pagehelper分页
    SpringBoot集成Mybatis(0配置注解版)
    高并发下接口幂等性解决方案
    SpringBoot全局配置文件
    干货
    Spring读取外部的资源配置文件—@PropertySource和@Value实现资源文件配置
  • 原文地址:https://www.cnblogs.com/zhangmingzhao/p/12877329.html
Copyright © 2011-2022 走看看