可視化埋點平臺元素曝光采集intersectionObserver思路實踐
正文
最近一直在開發(fā)可視化埋點系統(tǒng),其中元素的曝光埋點,就是借助了 intersectionObserver 這個原生 api。也是網(wǎng)上推薦度比較高的方案,同時 2022 年,該 api 兼容性也已經(jīng)很高,同時也有 polyfill,基本上使用無虞。
intersectionObserver 本身 api 非常簡單,但是在實際使用的過程中,由于可視化埋點的一些特殊性以及對埋點準確性的要求,還是遇到了一些 dom 變更后的邊緣場景,本文便是對這些邊緣場景的一個記錄及實現(xiàn)背后的一些考慮。
通常來講,元素的埋點是開發(fā)者主動在元素中直接埋點,而筆者正在開發(fā)的可視化埋點,在數(shù)據(jù)收集側的工作,是異步從后臺獲取用戶需要的元素 xpath,然后再通過 xpath 尋找到元素,調用 intersectionObserver 的 observe 方法進行監(jiān)聽元素的曝光。所以在進行監(jiān)聽的時機上,都是在元素掛載到 dom 后進行元素的監(jiān)聽,那么初次監(jiān)聽,是否會觸發(fā)其回調,便關系到曝光埋點的準確性。
同時,可視化埋點也監(jiān)聽頁面 dom 的變更,當變更后,需要解除舊元素的監(jiān)聽,同時增加對新元素的監(jiān)聽。那么如果多次監(jiān)聽同一元素,是否會多次觸發(fā)回調,也影響到曝光的準確性。
設計方案
為了快速上線第一版,筆者最初的設計方案為:
- 根據(jù)服務端返回的 xpath,尋找到對應的 dom 元素,對元素進行 observe
- 監(jiān)聽 dom 變更事件,當 dom 發(fā)生改變后,重新根據(jù) xpath 尋找 dom 元素,對元素進行再次 observe
在該方案中,存在幾個觸發(fā)時機可能導致的問題:
- 當監(jiān)聽發(fā)生在元素渲染到頁面后,首次監(jiān)聽的同時,是否會觸發(fā)回調(影響到初次曝光的準確性)
- 多次監(jiān)聽同一 dom 元素,是否會多次觸發(fā)回調(影響到 dom 變更,多次監(jiān)聽同一元素后,曝光的準確性)
測驗結論
- 當初次監(jiān)聽元素時,會立即觸發(fā)一次回調
- 多次監(jiān)聽同一元素,并不會多次觸發(fā)回調
在上述邏輯成立的情況下,筆者最初的方案其實是可以正常工作,對于初次曝光,雖然發(fā)生在元素渲染到 dom 之后,但是由于會立即觸發(fā)一次,故初次曝光能夠正常上報。而當 dom 發(fā)生變更后,消失的元素,雖然沒有調用 unobserve,但是由于該元素消失了,并不會影響后續(xù)曝光埋點的上報,所以并沒有帶來大的問題,而 dom 變更后,元素如果依然存在,雖然再次進行了監(jiān)聽,但是多次監(jiān)聽并不影響同一元素,所以其實也沒有問題。
對于第一版,上線后也確實能夠正常工作,但是對于沒有 unobserve 這一點,由于 js 的垃圾回收機制,必須是沒有引用后才會銷毀,而沒有 unobserve,那么內部必然會維護一份監(jiān)聽的元素的列表,保留了已經(jīng)從 dom 中移除的元素的引用,從而造成內存泄漏。
故需要做一些策略來避免該問題(不然代碼也會被吐槽),思路如下:
維護一份 xpath -> 元素的映射,當 dom 發(fā)生變更時,遍歷所有 xpath 尋找對應的元素,
如果元素同映射中一致,那么表示該元素沒有發(fā)生變更,此時可以直接忽略,什么都不做。
而如果元素發(fā)生變化,那么調用 unobserve 取消舊元素的監(jiān)聽,同時對新元素進行監(jiān)聽即可。
完整的偽代碼
// 工具函數(shù)命名很清晰,含義不贅述 import { getEleByXpath, getXpathByEle, debounce } from 'utils'; class track { constructor() { /* { xpath: '', id: ''id } */ this.config = {} // 存儲遠程服務端返回的埋點 xpath 信息 /* [xpath]: el, */ this.map = {} // 維護的 xpath -> el 映射 this.observer = null; // intersectionObserver 實例 } // 遠端獲取需要曝光點的元素 getConfig() { return fetch('xxx').then(res => { this.config = res; this.config.forEach(item => { // 初始化 xpath -> el 映射 this.map[item.xpath] = null; }) }); } // 監(jiān)聽 dom 變更 addDomtreeMutatorObserver() { // 不關心屬性變化 const config = { attributes: false, childList: true, subtree: true }; // 監(jiān)聽dom變更,消個抖 const observer = new MutationObserver(debounce(this.observe.bind(this), 200)); observer.observe(document.body, config); } // 監(jiān)聽元素曝光 observe() { // 此處可以加個 requestIdleCallback 來增強性能 this.config.forEach((item) => { const targetEl = getEleByXpath(item.xpath); // 新舊元素不一致才需要取消舊元素監(jiān)聽,增加新元素監(jiān)聽 if (targetEl !== this.map[item.xpath]) { // 元素存在,就監(jiān)聽 if (targetEl) { this.observer.observe(targetEl); } // 取消舊元素的監(jiān)聽 if (this.map[shadow.xpath]) { this.observer.unobserve(this.map[shadow.xpath]); } // 更新map中的el this.map[shadow.xpath] = targetEl; } // 一致,則什么都不做 }); } // 創(chuàng)建 intersectionObserver initObserver() { const callback = (entries) => { entries.forEach((entry) => { if (entry.intersectionRatio > 0.2) { const targetXpath = getXpath(entry.target); for (let i = 0; i < this.config.length; i++) { if (this.config[i].xpath === targetXpath) { // xpath一致 // 發(fā)送曝光埋點 } } } }); }; const observer = new IntersectionObserver(callback, { threshold: 0.2, }); this.observer = observer; } init() { this.getConfig().then(() => { // 初始化 intersectionObserver this.initObserver(); // 監(jiān)聽元素 this.observe(); // 監(jiān)聽 dom 元素變更 this.addDomtreeMutatorObserver(); }); } }
以上便是精簡后的偽代碼,其核心的優(yōu)化點便是維護 xpath -> el 的 map(說實話,一開始筆者其實就想到了 map,但是當時想的是拿 dom 引用作為 key,拿對象做 key 顯然是行不通的,就短暫放棄,先上線再說。后來想到用 xpath 做key,才有了這篇文章)。
其實本文的初衷是記錄一下 intersectionObserver 的一些邊緣情況及結論(其實還對 intersectionObserver 做了其他一些比較傻的測試),但是在文章編寫的過程中發(fā)現(xiàn)干巴巴的分析,顯得實在是無趣,故夾雜了實際的可視化埋點曝光采集的業(yè)務場景,希望沒有寫的很亂。
同時,當筆者徹底理清思路后,發(fā)現(xiàn)這優(yōu)化其實不值一提,只是筆者最初在設計可視化埋點方案時,由于對這些 api 的不熟悉,導致的一些迷迷糊糊的摸爬滾打經(jīng)驗,還望懂的同學別笑。
以上就是可視化埋點平臺元素曝光采集intersectionObserver思路實踐的詳細內容,更多關于intersectionObserver元素曝光采集的資料請關注腳本之家其它相關文章!
相關文章
借助FileReader實現(xiàn)將文件編碼為Base64后通過AJAX上傳
這篇文章主要介紹了借助FileReader實現(xiàn)將文件編碼為Base64后通過AJAX上傳的方法,包括后端對文件數(shù)據(jù)解碼并保存的PHP代碼,需要的朋友可以參考下2015-12-12js實現(xiàn)table添加行tr、刪除行tr、清空行tr的簡單實例
下面小編就為大家?guī)硪黄猨s實現(xiàn)table添加行tr、刪除行tr、清空行tr的簡單實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10jquery中l(wèi)ive()方法和bind()方法區(qū)別分析
這篇文章主要介紹了jquery中l(wèi)ive()方法和bind()方法區(qū)別,結合實例形式簡單分析了live()方法和bind()方法的功能、使用方法與用法區(qū)別,需要的朋友可以參考下2016-06-06ie8 不支持new Date(2012-11-10)問題的解決方法
使用JS的時候也碰到了如下問題,后來經(jīng)過修改,在IE8環(huán)境里,下面的代碼是可用的,下面與大家分享下ie8 不支持new Date的解決方法,有類似問題的朋友可以參考下2013-07-07個人網(wǎng)站留言頁面(前端jQuery編寫、后臺php讀寫MySQL)
這篇文章主要為大家介紹了個人網(wǎng)站的留言頁面,前端使用jQuery編寫、后臺利用php簡單讀寫MySQL數(shù)據(jù)庫,感興趣的小伙伴們可以參考一下2016-05-05boostrapTable的refresh和refreshOptions區(qū)別淺析
在使用bootstrapTable時,刷新數(shù)據(jù)有兩個方法refresh、refreshOptions,在其用法上有點區(qū)別,接下來通過本文給大家分享boostrapTable的refresh和refreshOptions的區(qū)別,需要的朋友可以參考下2017-01-01jquery根據(jù)錨點offset值實現(xiàn)動畫切換
點擊后僵硬的切換是不是很不爽,下面為大家介紹的是根據(jù)錨點offset值來實現(xiàn)動畫切換,喜歡的朋友不要錯過2014-09-09