像监听页面一样监听戈多的动态

1,038 阅读4分钟

不知道各位童鞋有木有看过 《等待戈多》 这部出名的荒诞戏剧 。其剧情大概就是 戈戈 与 狄狄 等待 戈多 的过程中发生的一些琐事,一共两幕。等了这么多年,也不知道 戈多 现在在哪,赴约了没有。

如果 戈戈 与 狄狄 像我们监听页面元素变化那样监听戈多的动态,是不是就不会出现空欢喜的状态?是不是就不用等得那么辛苦?是不是甚至可以主动去寻找戈多?

wait

说起监听页面元素变化,那么你可知道有哪些方法可以实现这个功能?

Object.defineProperty

关于 Object.defineProperty 这个属性大家应该很熟(毕竟是各类面经中的常客),但还是要简单介绍下~

Object.defineProperty 允许精确添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,能够在属性枚举期间呈现出来。

描述符可同时具有的键值:

configurableenumerablevaluewritablegetset
数据描述符YesYesYesYesNoNo
存取描述符YesYesNoNoYesYes

所以我们有以下这种效果:

gd1

代码如下:

'use strict'
Object.defineProperty(godot, 'style', {
    get() {
        return this.getAttribute('style')
    },
    set(data) {
        this.setAttribute('style', data)
        const distance = (noLeftTree.offsetLeft - this.offsetLeft)
        console.log(distance >= 51 ? '戈多没来,我们先各自干各自的活吧' : '戈多快到了,走,我们集合去')
    }
})
const whereIsGodot = start => {
    if (start) {
        let d = 0
        const godotRun = () => {
            if (noLeftTree.offsetLeft - 51 >= d) {
                setTimeout(() => {
                    d++
                    godot.style = `left: ${d}px`
                    godotRun()
                }, 16)
            }
        }
        godotRun()
    }
}

简单来说就是使用 Object.defineProperty 监听戈多的位置变化,然后当戈多移动到集合地点附近时,等待戈多的俩哥们就可以去赴约了。通过上述的代码,我们可以知道 whereIsGodot 函数只负责戈多的位置移动,但是监听权在等待戈多的两个人那里,这样保证了代码语义化的同时,耦合度也尽可能地小。

MutationObserver

Mmmmm,我一直以为 MutationObserver 是个新属性,直到我膝盖中了一箭看了can i use

caniuse

本来鱼头我也不知道有这属性,但是最近在工作上遇到了需要监听页面元素变动的场景,然后就了解到了这个API。

于是鱼头便看了文档,发现是个好牛逼的API。

dc

所以这到底是个啥?

简单来说就是一个可以监听 DOM Tree 变动的API,名字直译就是 “突变观察者”

WHATWG的定义,它的执行逻辑如下:

  1. 先执行监听的微任务队列;
  2. 执行完微任务队列之后就把所监听的记录封装成一个数组来处理;
  3. 然后返回处理结果。

所以具体怎么用?

突变观察者 是个构造器,它接受一个回调并返回一个 节点记录列表(sequence <MutationRecord> 以及 构造的参数对象(MutationObersver)

它有以下三个方法:

  1. observe(target, options):监听对象,接受两个参数,一个是监听的对象(target),一个是观察的选项(options);
  2. disconnect():断开监听的功能;
  3. takeRecords():清空监听的队列,并返回结果。

options选项可选参数(以下属性可设置为true):

  1. childList:监听目标子节点的变化;
  2. attributes:监听目标属性的变化;
  3. characterData:监听目标数据的变化;
  4. subtree:监听目标以及其后代的变化;
  5. attributeOldValue:监听目标属性变化前的具体值;
  6. characterDataOldValue:监听目标数据变化前的具体值;
  7. attributeFilter:不需要监听的属性列表(此属性填入过滤的属性列表)。

如何监听戈多的位置?

下面我们就通过实际的代码来监听戈多的位置变化。

效果还是如同上图。

代码如下:

const godot = document.querySelector('#godot')
const config = {
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true,
    attributeOldValue: true,
    characterDataOldValue: true
}
const mutationCallback = mutationsList => {
    const [
        {
            target: {
                offsetLeft: godotPos
            }
        }
    ] = mutationsList
    const distance = (noLeftTree.offsetLeft - godotPos)
    console.log(distance >= 51 ? '戈多没来,我们先各自干各自的活吧' : '戈多快到了,走,我们集合去')
}
const observer = new MutationObserver(mutationCallback)
observer.observe(godot, config)
const whereIsGodot = start => {
    if (start) {
        let d = 0
        const godotRun = () => {
            if (noLeftTree.offsetLeft - 51 >= d) {
                setTimeout(() => {
                    d++
                    godot.style = `left: ${d}px`
                    godotRun()
                }, 16)
            } else {
                observer.disconnect()
            }
        }
        godotRun()
    }
}

因为鱼头在业务需要对某个已经完善的功能在部分操作监听数据变动,如果对原来的代码进行改动,也不是一件轻松的事,而且这样子代码太冗长,耦合度也会较高,所以就选择了用 突变观察者 来实现,效果还是不错的。

Intersection Observer

除了监听元素的变动,还有什么方式可以知道戈多的位置呢?

如果有那就是 Intersection Observer 了。

这又是个啥?

戈多心想:“又来一个Observer ?别监听了,我去找你们就是了,嘤嘤嘤。 ”

委屈

IntersectionObserver 直译是 “交叉观察者” ,这个API使开发人员能够监听目标元素与根(祖先或视口)元素交叉状态的方法。

它的用法跟 MutationObserver 相似,同样是个构造器,它接受一个 回调函数(callback(entries)) 以及 可选参数对象(options)

所以又怎么用?

首先 callback 会返回一个 监听属性对象(IntersectionObserverEntry) ,其具体属性如下:

  1. time:可见性发生变化的时间,是个双精度的毫秒时间戳;
  2. rootBounds:根元素的盒子区域信息,有根元素则返回 getBoundingClientRect() 的值,没有则返回 null
  3. boundingClientRect:监听元素的盒子区域信息;
  4. intersectionRect:监听元素与根元素的交叉区域信息;
  5. isIntersecting:判断监听元素是否与根元素相交,返回布尔值;
  6. intersectionRatio:监听元素的可见比例,即intersectionRect / boundingClientRect 完全可见时为1,完全不可见时小于等于0;
  7. target:监听的目标元素。

options 可选参数如下:

  1. root:与监听对象相交的根元素,如果没有,返回隐式根;
  2. rootMargin:跟CSS的margin一样,发生交叉的偏移量;
  3. threshold:触发回调的阈值,填入数组,范围在0~1之间,决定发生监听事件的交叉比例。

可选择方法如下:

  1. IntersectionObserver.observe():开始监听;
  2. IntersectionObserver.disconnect():停止监听;
  3. IntersectionObserver.takeRecords():返回所有观察目标的 IntersectionObserverEntry 对象数组;
  4. IntersectionObserver.unobserve():使IntersectionObserver停止监听特定目标元素。

戈多,你今晚到底是来还是不来?

所以怎么用这个API来监听戈多的位置呢?

先看效果(真特么简陋)

godot3

代码如下:

<style>
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    html,
    body {
        width: 100%;
        height: 200%;
    }
    noLeftTree {
        position: fixed;
        left: 0;
        top: 0;
        width: 100%;
        height: 100px;
        background: #FFF;
    }
    godot,
    estragon,
    vladimir {
        position: absolute;
        width: 50px;
        height: 50px;
        border-radius: 50%;
        border: 1px solid;
        text-align: center;
    }
    estragon {
        top: 0;
        left: 0;
    }
    vladimir {
        top: 0;
        right: 0;
    }
    godot {
        left: calc(50% - 25px);
        top: 1000px;
    }
</style>
<noLeftTree id="noLeftTree">
    <estragon id="estragon">戈戈</estragon>
    <vladimir id="vladimir">狄狄</vladimir>
</noLeftTree>
<godot id="godot">戈多</godot>
<script>
    'use strict'
    const godot = document.querySelector('#godot')
    const noLeftTree = document.querySelector('#noLeftTree')
    const ioCallback = entries => {
        console.log(entries[0].intersectionRatio <= 0 ? '戈多没来,我们先各自干各自的活吧' : '戈多快到了,走,我们集合去')
    }
    const ioConfig = {
        threshold: [0, 0.25, 0.5, 0.75, 1]
    }
    const io = new IntersectionObserver(ioCallback, ioConfig)
    io.observe(godot)
</script>

后记

其实如果肯花时间去研究,利用好上述三个API,是可以实现很多很有趣的效果的,上面的只是一个初尝的DEMO,真正在项目里是可以实现很多很重要的功能。

不过戈戈 与 狄狄也等待戈多快70年了,就像痴情的女生等待远走的渣男一样,就是不来好歹也给个音信啊。

戈多心想:“我不过是迷路了么,嘤嘤嘤”

img

如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。 鱼头的微信号是:krisChans95