- 作者:陳大魚頭
- github: KRISACHAN
不知道各位童鞋有木有看過 《等待戈多》 這部出名的荒誕戲劇 。其劇情大概就是 戈戈 與 狄狄 等待 戈多 的過程中發生的一些瑣事,一共兩幕。等了這麼多年,也不知道 戈多 現在在哪,赴約了沒有。
如果 戈戈 與 狄狄 像我們監聽頁面元素變化那樣監聽戈多的動態,是不是就不會出現空歡喜的狀態?是不是就不用等得那麼辛苦?是不是甚至可以主動去尋找戈多?
說起監聽頁面元素變化,那麼你可知道有哪些方法可以實現這個功能?
Object.defineProperty
關於 Object.defineProperty
這個屬性大家應該很熟(畢竟是各類面經中的常客),但還是要簡單介紹下~
Object.defineProperty
允許精確添加或修改對象的屬性。通過賦值操作添加的普通屬性是可枚舉的,能夠在屬性枚舉期間呈現出來。
描述符可同時具有的鍵值:
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
數據描述符 | Yes | Yes | Yes | Yes | No | No |
存取描述符 | Yes | Yes | No | No | Yes | Yes |
所以我們有以下這種效果:
代碼如下:
'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 。
本來魚頭我也不知道有這屬性,但是最近在工作上遇到了需要監聽頁面元素變動的場景,然後就瞭解到了這個API。
於是魚頭便看了文檔,發現是個好牛逼的API。
所以這到底是個啥?
簡單來說就是一個可以監聽 DOM Tree 變動的API,名字直譯就是 “突變觀察者” 。
按WHATWG的定義,它的執行邏輯如下:
- 先執行監聽的微任務隊列;
- 執行完微任務隊列之後就把所監聽的記錄封裝成一個數組來處理;
- 然後返回處理結果。
所以具體怎麼用?
突變觀察者 是個構造器,它接受一個回調並返回一個 節點記錄列表(sequence <MutationRecord>
) 以及 構造的參數對象(MutationObersver)。
它有以下三個方法:
- observe(target, options):監聽對象,接受兩個參數,一個是監聽的對象(target),一個是觀察的選項(options);
- disconnect():斷開監聽的功能;
- takeRecords():清空監聽的隊列,並返回結果。
options選項可選參數(以下屬性可設置爲true):
- childList:監聽目標子節點的變化;
- attributes:監聽目標屬性的變化;
- characterData:監聽目標數據的變化;
- subtree:監聽目標以及其後代的變化;
- attributeOldValue:監聽目標屬性變化前的具體值;
- characterDataOldValue:監聽目標數據變化前的具體值;
- 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) ,其具體屬性如下:
- time:可見性發生變化的時間,是個雙精度的毫秒時間戳;
- rootBounds:根元素的盒子區域信息,有根元素則返回
getBoundingClientRect()
的值,沒有則返回null
; - boundingClientRect:監聽元素的盒子區域信息;
- intersectionRect:監聽元素與根元素的交叉區域信息;
- isIntersecting:判斷監聽元素是否與根元素相交,返回布爾值;
- intersectionRatio:監聽元素的可見比例,即
intersectionRect / boundingClientRect
完全可見時爲1,完全不可見時小於等於0; - target:監聽的目標元素。
options 可選參數如下:
- root:與監聽對象相交的根元素,如果沒有,返回隱式根;
- rootMargin:跟CSS的
margin
一樣,發生交叉的偏移量; - threshold:觸發回調的閾值,填入數組,範圍在0~1之間,決定發生監聽事件的交叉比例。
可選擇方法如下:
- IntersectionObserver.observe():開始監聽;
- IntersectionObserver.disconnect():停止監聽;
- IntersectionObserver.takeRecords():返回所有觀察目標的 IntersectionObserverEntry 對象數組;
- IntersectionObserver.unobserve():使
IntersectionObserver
停止監聽特定目標元素。
戈多,你今晚到底是來還是不來?
所以怎麼用這個API來監聽戈多的位置呢?
先看效果(真特麼簡陋)
代碼如下:
<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年了,就像癡情的女生等待遠走的渣男一樣,就是不來好歹也給個音信啊。
戈多心想:“我不過是迷路了麼,嚶嚶嚶”
如果你、喜歡探討技術,或者對本文有任何的意見或建議,你可以掃描下方二維碼,關注微信公衆號“ 魚頭的Web海洋 ”,隨時與魚頭互動。歡迎!衷心希望可以遇見你。