怎么断定元素呈现/离开屏幕可视区呢?
本文经过分析rax-appear库(能够疏忽rax,直接简略把它以为react即可),来取得一些启发,那咱们就开始吧!

平常开发中,有遇到一些这样的需求

  • 判别导航栏滚出屏幕时,让导航栏变为fixed状况,反之则正常放置在文档流中
  • 当一个产品卡片/广告呈现时,需求对其进行曝光埋点
  • 在瀑布流中,能够经过断定“加载更多”div的呈现,来发起下一页的恳求
  • 对图片们做懒加载,优化网页功能

本文结构如下:

  • IntersectionObserver的用法
  • IntersectionObserver的polyfill
  • DOM元素监听onAppear/onDisappear工作
  • DOM元素设置appear/disappear相关特点

IntersectionObserver的用法

Intersection Observer API答应你注册一个回调函数,当呈现下面的行为时,会触发该回调:

  • 每逢一个元素进入/退出与另一个元素(或视口)的交集时,或许当这两个元素之间的交集产生指定量的变化时
  • 首次观测该元素时会触发 不用时,还需求停止对一切方针元素可见性变化的调查,调用disconnect办法即可

用法

let options = {
  root: document.querySelector("#scrollArea"), // 调查方针元素的容器
  rootMargin: "0px", // room容器的margin,核算交集前,经过margin扩大/缩小root的高宽
  threshold: 1.0, // 交集占多少时,才履行callback。为1便是要等方针元素的每一个像素都进入root容器时才履行,默以为0,即只要方针元素刚刚1px进入root容器就触发
};
// 回调函数
let callback = (entries, observer) => {
  entries.forEach((entry) => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
    if (entry.isIntersecting) {
      let elem = entry.target;
      if (entry.intersectionRatio >= 0.75) {
        intersectionCounter++;
      }
    }
  });
};
let observer = new IntersectionObserver(callback, options);
// 方针元素
let target = document.querySelector("#listItem");
// 直到咱们为调查者设置一个方针元素(即使方针当时不行见)时,回调才开始首次履行
observer.observe(target);

rootMargin不同时的作用,一图胜千言:

【Appear】怎么完成元素可见/不行见的监听

图片出处:velog.io/@katanazero…

关于回调函数

需求留意的是回调函数是在主线程上履行的,如果它里面存在履行耗时长的使命,就会阻止主线程做其他工作,势必会影响到页面的渲染。关于耗时长的使命,建议放到Window.requestIdleCallback(),或许放到新的宏使命中去

const handleIntersection = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // 元素进入视口时履行的使命
      window.requestIdleCallback(() => {
        // 在浏览器闲暇时履行的使命
        console.log('元素进入视口并浏览器处于闲暇状况');
      });
    }
  });
};
const options = {
  root: null,
  rootMargin: '0px',
  threshold: 0.5,
};
const observer = new IntersectionObserver(handleIntersection, options);
const targetElement = document.getElementById('target');
observer.observe(targetElement);

奇特的问题&思路

stack overflow上有一个这样的问题,他用IntersectionObserver来监听一个列表的翻滚,当翻滚慢点时候表现正常,可是翻滚非常快的时候,会发现列表元素并不能被Observer来捕获,想问这是什么造成的?

一位大佬解说说,IO的首要方针是查看某个元素是否对人眼可见,依据Intersection Observer标准,其方针是供给一个简略且最佳的解决方案来推延或预加载图画和列表、检测商务广告可见性等。但当移动翻滚条的速度快于这些查看产生的速度,也便是当IO过于频频,可能无法检测到某些可见性更改,甚至这个没有被检测到的元素,都还没有被渲染。

关于这个问题,供给了如下解决方案,即经过核算本次观测到的列表元素范围变化,知道了当时翻滚的minId和maxId,就能知道列表的哪部分被检测了,然后进行事务处理。

let minId = null;
let maxId = null;
let debounceTimeout = null;
function applyChanges() {
  console.log(minId, maxId);
  const items = document.querySelectorAll('.item');
  // perform action on elements with Id between min and max
  minId = null;
  maxId = null;
}
function reportIntersection(entries) {
  clearTimeout(debounceTimeout);
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const entryId = parseInt(entry.target.id);
      if (minId === null || maxId === null) {
        minId = entryId;
        maxId = entryId;
      } else {
        minId = Math.min(minId, entryId);
        maxId = Math.max(maxId, entryId);
      }
    }
  });
  debounceTimeout = setTimeout(applyChanges, 500);
}
const container = document.querySelector('#container');
const items = document.querySelectorAll('.item');
const io = new IntersectionObserver(reportIntersection, container);
let idCounter = 0;
items.forEach(item => {
  item.setAttribute('id', idCounter++);
  io.observe(item)
});

IntersectionObserver的polyfill

经过setInterval或许监听resize、scroll、MutationObserver(用于调查 DOM 树的变化并在产生变化时触发回调函数。它能够监听 DOM 的插入、删去、特点修正、文本内容修正等变化)来触发检测,这里只贴了一些要害路径代码,想了解更多的能够网上搜搜看。

_proto._monitorIntersections = function _monitorIntersections() {
    if (!this._monitoringIntersections) {
      this._monitoringIntersections = true;
      // If a poll interval is set, use polling instead of listening to
      // resize and scroll events or DOM mutations.
      if (this.POLL_INTERVAL) {
        this._monitoringInterval = setInterval(this._checkForIntersections, this.POLL_INTERVAL);
      } else {
        addEvent(window, 'resize', this._checkForIntersections, true);
        addEvent(document, 'scroll', this._checkForIntersections, true);
        if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) {
          this._domObserver = new MutationObserver(this._checkForIntersections);
          this._domObserver.observe(document, {
            attributes: true,
            childList: true,
            characterData: true,
            subtree: true
          });
        }
      }
    }
  }

然后经过getBoundingClientRect来核算方针元素和root容器的intersection

【Appear】怎么完成元素可见/不行见的监听

const intersectionRect = computeRectIntersection(parentRect, targetRect);
/**
 * Returns the intersection between two rect objects.
 * @param {Object} rect1 The first rect.
 * @param {Object} rect2 The second rect.
 * @return {?Object} The intersection rect or undefined if no intersection
 *     is found.
 */
function computeRectIntersection(rect1, rect2) {
  var top = Math.max(rect1.top, rect2.top);
  var bottom = Math.min(rect1.bottom, rect2.bottom);
  var left = Math.max(rect1.left, rect2.left);
  var right = Math.min(rect1.right, rect2.right);
  var width = right - left;
  var height = bottom - top;
  return width >= 0 && height >= 0 && {
    top: top,
    bottom: bottom,
    left: left,
    right: right,
    width: width,
    height: height
  };
}

DOM元素监听onAppear/onDisappear工作

raxpollfill需求在Rax环境(简略以为Rax=React)运转,开发者在使用时,需求给元素绑定onAppear和onDisappear工作:

<div
  id="myDiv"
  onAppear={(event) => {
    console.log('appear: ', event.detail.direction);
  }}
  onDisappear={() => {
    console.log('disappear: ', event.detail.direction);
  }}
>
  hello
</div>

可是上面的代码(React的jsx)不能运转在浏览器中,还需求经过编译,然后凭借react的运转时跑在浏览器中,归根到底会变为如下形式:

<div id="myDiv">
  hello
</div>
<script>
  const myDiv = document.getElementById('myDiv');
  function handlerAppear(event) {
    console.log('appear: ', event.detail.direction);
  }
  myDiv.addEventListener('appear', handlerAppear);
  function handlerDisAppear(event) {
    console.log('disappear: ', event.detail.direction);
  }
  myDiv.addEventListener('disappear', handlerDisAppear);
</script>

上面这段代码,这个div元素将被作为IntersectionObserver调查的方针对象,当满意了交集判别的条件,就会触发onAppear的回调,能够在回调中处理自定义的逻辑。就像onClick一样,当点击产生后,履行onClick回调的内容,那这是怎么做到的呢?

  • 阻拦原型办法:需求找一个时机(addEventListener的回调触发前),为当时元素绑定IntersectionObserver工作
  • 自定义工作:当元素的Observer的callback触发后,需求抛出onAppear/onDisappear工作,履行工作回调

先经过hack DOM元素上的原型办法,当eventName为appear的工作时,给元素绑定上IntersectionObserver,即履行observerElement(this),当eventName为其他工作时,不进行特殊处理。

// hijack Node.prototype.addEventListener
const injectEventListenerHook = (events = [], Node, observerElement) => {
  let nativeAddEventListener = Node.prototype.addEventListener;
  Node.prototype.addEventListener = function (eventName, eventHandler, useCapture, doNotWatch) {
    const lowerCaseEventName = eventName && String(eventName).toLowerCase();
    const isAppearEvent = events.some((item) => (item === lowerCaseEventName));
    if (isAppearEvent) observerElement(this);
    nativeAddEventListener.call(this, eventName, eventHandler, useCapture);
  };
  return function unsetup() {
    Node.prototype.addEventListener = nativeAddEventListener;
    destroyAllIntersectionObserver();
  };
};
injectEventListenerHook(['appear', 'disappear'], window.Node, observerElement)

那当元素在root容器内的交集产生变化时,触发了Observer的回调,里面会履行dispatchEvent,如下:

function handleIntersect(entries) {
  entries.forEach((entry) => {
    const {
      target,
      boundingClientRect,
      intersectionRatio
    } = entry;
    const { currentY, beforeY } = getElementY(target, boundingClientRect);
    // is in view
    if (
      intersectionRatio > 0.01 &&
      !isTrue(target.getAttribute('data-appeared')) &&
      !appearOnce(target, 'appear')
    ) {
      target.setAttribute('data-appeared', 'true');
      target.setAttribute('data-has-appeared', 'true');
      // 首要重视这里
      target.dispatchEvent(createEvent('appear', {
        direction: currentY > beforeY ? 'up' : 'down'
      }));
    } else if (
      intersectionRatio === 0 &&
      isTrue(target.getAttribute('data-appeared')) &&
      !appearOnce(target, 'disappear')
    ) {
      target.setAttribute('data-appeared', 'false');
      target.setAttribute('data-has-disappeared', 'true');
      // 首要重视这里
      target.dispatchEvent(createEvent('disappear', {
        direction: currentY > beforeY ? 'up' : 'down'
      }));
    }
    target.setAttribute('data-before-current-y', currentY);
  });
}

当履行了dispatchEvent,发出appear工作,也就会触发appear的addEventListener里的回调,就履行自定义逻辑。

DOM元素设置appear/disappear相关特点

用到了DOM的api,即setAttribute,经过这个api把appear和disppear的信息露出在方针元素上,比方露出元素是否是首次曝光,元素间隔root容器的y值间隔等等,便利事务开发获取相应值,然后履行事务自定义逻辑。


【Appear】怎么完成元素可见/不行见的监听

祝贺你,看到了文章的最后,希望能帮助到你,有任何疑问,欢迎留言,共同进步!