Quiet Storm Lyn
  1. 1 Quiet Storm Lyn
  2. 2 かかってこいよ NakamuraEmi
  3. 3 Last Surprise Lyn
  4. 4 Life Will Change Lyn
  5. 5 One Last You Jen Bird
  6. 6 Time Bomb Veela
  7. 7 Flower Of Life 发热巫女
  8. 8 Hypocrite Nush
  9. 9 Warcry mpi
  10. 10 Libertus Chen-U
  11. 11 The Night We Stood Lyn
2019-02-17 02:28:59

从延迟加载谈DOM的状态监听

最近发现了一组相当实用的API,专用于DOM的状态变化监听,由于兼容性的问题好像被社区提及不多,不过生产环境下做好特性检测还是很有实用价值的,有必要了解一下。

延迟加载实现

谈到API具体内容之前,还是先从场景入手更有意义。以平时前端接触最多的延迟加载也就是懒加载为例,常用实现方式是监听scroll事件做处理,简单实现如下:

function lazyload() {
    const images = document.querySelectorAll('img');
    const len = images.length;
    let n = 0;         
    return function() {
        const viewHeight = document.documentElement.clientHeight;
        for (let i = n; i < len; i++) {
            const top = images[i].getBoundingClientRect().top();
            if(top < viewHeight) {
                if(images[i].getAttribute('src') === 'images/loading.gif') {
                    images[i].src = images[i].dataset.src;
                }
            n = n + 1;
        }
    }
}
const loadImages = lazyload();
loadImages();
window.addEventListener('scroll', loadImages, false);

这种思路的实现有两个问题,一是scroll的频繁触发,需要再手动的处理一下节流。二是获取元素位置的同时会造成重排,影响性能。再理一下延迟加载的核心,就是在于检测元素是否在视口内再执行接下来的逻辑,那么这个为此而生的API就是最合适的实现思路——IntersectionObserver。先看实现。

const images = document.querySelectorAll('img');
const iO = new IntersectionObserver((items) => {
    items.forEach(item => {
        if (item.isIntersecting) {
            item.target.src = item.target.dataset.src;
            iO.unobserve(item.target);
        }
    }, {
        rootMargin: '100px 0px'
    });
})
for (let i = 0; i < images.length; i++) {
    iO.observe(images[i]);
}

相当清晰易懂的代码,不仅规避了位置计算,同时也完全规避了滚动事件改为异步触发,性能比节流又上升了一个档次。简单来说,这个API就是专用于检测DOM元素是否处于视口的状态,下面就看一下API的详细使用。

DOM视口状态监听——IntersectionObserver

参数

该API原生提供为IntersectionObserver构造函数,接受如下两个参数:

  • callback: 被观察元素可见性变化时,该回调被调用
  • option:配置对象,配置观察相关的属性

callback的参数为一个数组,每个成员都是被观察对象中,可见性发生变化的对象,官方封装后称呼为IntersectionObserverEntry对象,其上有如下属性:

  • time: 可见性变化发生的时间,单位为毫秒
  • target: 被观察元素,即原始DOM对象
  • rootBounds: 根元素的矩形区域信息,即getBoundingClientRect()返回值,如果没有设置根元素则相对于视口,其值返回null
  • boundingClientRect:被观察元素的矩形区域信息
  • intersectionRect:被观察元素与根元素的交叉区域信息
  • intersectionRatio:被观察元素的可见比例
  • isIntersecting:判断被观察元素是否和根元素相交

option详细配置如下:

  • root:设置被观察元素的根元素,很好理解,可见性的判断不只局限于窗口,同样容器里也可以存在滚动条影响可见性。
  • rootMargin: 添加到根矩形区域的偏移量。简单理解为交叉判定范围的设置就可以了,例如上例延迟加载中设置为100px 0,即可在被观察元素进入视口的前100px范围就触发callback
  • thresholds: 回调函数触发的门槛。可以相对于rootMargin理解,该值代表了回调函数触发的偏移。正常情况下,进入视口就触发回调,可以理解为该值是0。当该值为1时,就表示完全进入视口时触发。单位为数组,可以定义多个触发时机。

实例方法

  • observe: 为观察器添加一个被观察元素,参数为需要被观察的DOM
  • unobserve:移除特定元素的观察
  • takeRecords:理解这个方法需要先理解观察器的异步机制,当观察器观察到相交动作时,并不会立刻执行callback,而是会调用 window.requestIdleCallback包装我们的callback进入异步队列等待,最大延迟为100ms。
requestIdleCallback(() => {
  if (entries.length > 0) {
    callback(entries, observer)
  }
}, {
  timeout: 100
})

如果你某个需求确实需要知道相交动作发生的确切时间点,就需要调用这个方法获取Entry对象的信息。需要注意的是调用该方法后会清空当前Entry队列,换句话说,callback也就不会执行了。

  • disconnect: 停止观察器的所有监听工作

DOM变化状态监听——MutationObserver

和IntersectionObserver属于同系列的API——MutationObserver,用于监听DOM的变动,变动又可以细分为子节点变动(增删改),属性变动或节点内容的变动。实现原理和IntersectionObserver类似。

参数

同样需要实例化一个观察器,接受 callback参数,不同的是不需要option了,观察的option具体到每个观察对象去设置。

onst ob = new MutationObserver(callback);

Callback的参数同样为一个数组,每个成员都是被观察的对象中,DOM发生变化的对象,官方封装后称呼为MutationRecord 对象。其上的属性有:

  • type:观察的变动类型
  • target: 被观察的原始DOM节点
  • addedNodes:新增的DOM节点
  • removedNodes:删除的DOM节点
  • previousSibling:被新增或删除节点的前一个同级节点
  • nextSibling:被新增或删除的下一个同级节点
  • AttributeName: 发生变动的属性
  • oldValue: 根据被动类型返回变动前的值。

实例方法

  • observe: 为观察器添加一个被观察元素,同时接受一个option对象设置观察选项
{
    childList: true, // 是否观察子节点变动
    attributes: true, // 是否观察属性变动
    characterData: true, // 是否观察节点内容变动
    subTree: true, // 是否观察所有后代节点变动
    attributeOldValue: true, // 观察属性变动时是否记录变动前的值
    characterDataOldValue: true, // 观察节点内容变动时是否记录变动前的值
    attributeFilter: ['class'], // 表示需要观察的特定属性过滤
}
  • disconnect: 停止观察器的所有监听工作,需要注意该observer没有提供针对单个元素的unobserve方法,我也不知道为什么
  • takeRecords: 既然原理和IntersectionObserver一样,这个方法也就不多解释了。

实例

使用上来说,由于现在的View框架基本上接手了DOM变动操作,应用场景不如上个观察器广泛。不过需要提一点的就是Vue早期版本的nextTick借助了其特性的异步实现,相对于setTimeout的执行时机会更靠前一些。

 var counter = 1
 var observer = new MutationObserver(nextTickHandler)
 var textNode = document.createTextNode(String(counter))
 observer.observe(textNode, {
     characterData: true
 })
timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
}

DOM尺寸状态监听——ResizeObserver

同理,除了监听节点的增删改之外,也可以用该API监听节点的尺寸变化。这里就不详细介绍了,一个原因是使用上相当简单,同样是实例化观察器加上callback参数,而且不需要任何option。二是这个API实在是太新了,是这三兄弟兼容性最差的一个,使用场景有限可替代性也强,专门去引入一段polyfill好像也不是很值得,相对于以上两类观察器而言比较鸡肋,我的建议是属于了解有这么个东西就好,现在这个环境不推荐使用。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「JavaScript」 标签。