JavaScript如何動態(tài)監(jiān)聽DOM元素高度詳解
背景
考慮這樣一種情況,產(chǎn)品同學希望達到以下功能:
在我們的網(wǎng)頁中有一個固定區(qū)域,這個區(qū)域會用于渲染從后端拉取的含有圖片等資源的富文本字符串。
他需要在內(nèi)容不超過一個最大高度的時候完全顯示所有內(nèi)容,超過最大內(nèi)容后僅展示最大高度范圍內(nèi)的內(nèi)容,超出部分隱藏,并通過一個按鈕 “展示更多” 來給用戶展示更多的選擇。
在這看似簡單的需求當中,其實涉及到了一個難點,那就是怎樣動態(tài)的監(jiān)聽到內(nèi)容區(qū)域的高度變化?
因為在這里面會含有圖片資源,他們在渲染的時候會發(fā)起網(wǎng)絡請求,等待圖片加載完成后觸發(fā)瀏覽器重排,該區(qū)域的高度被撐開。
因此,內(nèi)容區(qū)域的高度是動態(tài)變化,且變化的時間點是未知的,那么怎樣知道我們的內(nèi)容區(qū)高度發(fā)生了變化呢?
為此我做了以下幾種嘗試:
- MutationObserver
- IntersectionObserver
- ResizeObserver
- 監(jiān)聽所有資源的 onload 事件
- iframe(推薦)
MutationObserver
MutationObserver 接口提供了監(jiān)視對 DOM 樹所做更改的能力。它被設計為舊的 Mutation Events 功能的替代品,該功能是 DOM3 Events 規(guī)范的一部分。
observe(target, options)
這個方法會根據(jù)傳入的 options 配置,觀察 DOM 樹中的單個 Node 或者所有的子孫節(jié)點的變化。
他一共有七個屬性,這里就不一一介紹了,可以通過 MutationObserverInit 來獲取相應的介紹.
那么我們要怎么使用這個 API 來監(jiān)聽目標區(qū)域的高度變化呢?
首先我們要創(chuàng)建對該區(qū)域的 dom 根結點引用:
const Details = () => { // useRef創(chuàng)建引用 const contentRef = useRef(); const [height, setHeight] = useState(-1); const [observer, setObserver] = useState<MutationObserver>(null!); useEffect(() => { const observer = new MutationObserver((mutationList) => { if (height !== contentRef.current?.clientHeight) { console.log('高度變化了!'); setHeight(contentRef.current.clientHeight); } }); setObserver(observer); }, []); useEffect(() => { if (!observer || !contentRef.current) return observer.observe(contentRef, { childList: true, // 子節(jié)點的變動(新增、刪除或者更改) attributes: true, // 屬性的變動 characterData: true, // 節(jié)點內(nèi)容或節(jié)點文本的變動 subtree: true// 是否將觀察器應用于該節(jié)點的所有后代節(jié)點 }); }, [contentRef.current, observer]); // 綁定ref return<div className="content" dangerouslySetInnerHTML={{ __html: details }} style={{ maxHeight }} ref={contentRef} /> }
經(jīng)過上面的一番操作之后,發(fā)現(xiàn)根本達不到效果,因為我們的 css 屬性根本沒有發(fā)生變化(我們是通過 maxHeight 來約束容器的高度的), 但是資源加載完畢之后,瀏覽器重排根本沒有產(chǎn)生 css 屬性的變化,它的高度是自動計算的
因此這個方案無濟于事!但是它確實可以監(jiān)聽到認為修改容器的高度產(chǎn)生的變化,比如:contentRef.current.style.height = ‘1000px’,這個 api 是可以監(jiān)聽到這一操作的,但是并不符合我們的場景
此外,它的瀏覽器兼容性也還行:
IntersectionObserver
經(jīng)過激情編碼,最后發(fā)現(xiàn) MutationObserver 根本達不到我們想要的效果之后,其實我的心態(tài)已經(jīng)產(chǎn)生了一些變化,不過不要緊!
我們可以換一種思路,既然我們無法通過監(jiān)聽容器的高度變化來展示相應的 “展開更多” 操作,那么我們可不可以將這個 “展開更多” 固定到一個位置上,然后超出部分隱藏,
當我們的內(nèi)容自動撐開,達到指定高度后,我們這個 “展開更多” 的操作的按鈕就顯示出來了,聽上去不錯,能達到要求!廢話不多說,開擼!
因為這里只涉及到相應的 css 樣式的書寫,就不做展示了。
經(jīng)過處理之后,確實在容器高度小于指定高度的時候,“展示更多” 按鈕不會展示,超過最大值之后,會將該按鈕展示出來,
但是也遇到了一個問題,操作按鈕是有高度的,如果我們的內(nèi)容高度介于最大高度 - 按鈕高度 到 容器的最大高度之間, 按鈕會產(chǎn)生顯示一部分,同時又隱藏一部分的效果,這可不是我們想要的!
顯然這種效果是不符合要求的,我們的 “展示更多” 按鈕,只有兩種狀態(tài),要么全部展示,要么不展示,沒有這種部分展示的效果
因此我查閱了相關資料,了解到了 IntersectionObserver 這個 API,它可以監(jiān)聽一個元素是否進入用戶視野,它的相關使用方法可以參考這篇文章:IntersectionObserver API 使用教程
它使用起來和 MutationObserver 幾乎一樣,只是名字不一樣而已
它監(jiān)聽的值里面有一個比較重要的屬性:intersectionRatio
借助這個 API,我的設計思路是這樣的:
當用戶滾動網(wǎng)頁的時候(或者不滾動,此時目標區(qū)域已經(jīng)出現(xiàn)在屏幕中),可以得到 intersectionRatio 的值,通過判斷這個值是否等于 1 來決定要不要展示 “展示更多” 按鈕
但經(jīng)過我的編碼實現(xiàn)后,發(fā)現(xiàn)滾動事件發(fā)生的時候,intersectionRatio 的變化是不可靠的,有時候完全可見了,但是它并不等于 1。經(jīng)過多輪實驗,結果依然如此。但是它確實可以用來判斷一個元素是否進入用戶視野
由于使用上結果的不可靠,我放棄這個方案(可能是我使用方式上出了問題)
它的各瀏覽器兼容性如下:
ResizeObserver
顧名思義,這個 API 就是專門監(jiān)聽 DOM 尺寸變化的,只不過它還處于試驗階段,各瀏覽器的兼容性很差,所以基本不考慮
具體使用方法可以參考這篇文章:檢測 DOM 尺寸變化 JS API ResizeObserver 簡介
它現(xiàn)階段各瀏覽器的兼容性情況:
監(jiān)聽所有資源的 onload 事件
既然上述方法都不行,那么我絞盡腦汁,又想出了另外一種方法:監(jiān)聽所有帶有 src 屬性的 DOM 元素的 onload 事件,通過他的回調(diào)來判斷當前容器的高度情況
這種實現(xiàn)方式,在思路上是完全符合目的的,具體做法參考如下:
const [height, setHeight] = useState(-1); const [showMore, setShowMore] = useState(false); // contentRef 的定義見 MutationObserver 一節(jié) useEffect(() => { const sources = contentRef.current.querySelectorAll("[src]"); sources.onload = () => { const height = contentRef?.current?.clientHeight ?? 0; const show = height >= parseInt(MAX_HEIGHT, 10); setHeight(height); setShowMore(show); }; }, []);
通過這種方式可以實現(xiàn)對富文本中的圖片進行加載后,對容器高度進行相應的判斷。
但是這種方式,存在不確定性,即無法判斷是否找齊了所有高度由內(nèi)容撐開的資源。
Iframe
這是終極方案,也是在此背景中所采用的方案。
既然 window 可以監(jiān)聽到 resize 事件,那么我們就可以利用 iframe 來達到同樣的效果,具體做法就是在容器里面嵌套一個隱藏的高度為 100% 的 iframe,通過監(jiān)聽他的 resize 事件,來判斷當前容器的高度。
話不多說,具體實現(xiàn)方式如下:
const Detail: FC<{}> = () => { const ref = useRef<HTMLDivElement>(null); const ifr = useRef<HTMLIFrameElement>(null); const [height, setHeight] = useState(-1); const [showMore, setShowMore] = useState(false); const [maxHeight, setMaxHeight] = useState(MAX_HEIGHT); const introduceInfo = useAppSelect( (state) => state.courseInfo?.data?.introduce_info ?? {} ); const details = introduceInfo.details ?? ""; const isFolded = maxHeight === MAX_HEIGHT; const onresize = useCallback(() => { const height = ref?.current?.clientHeight ?? 0; const show = height >= parseInt(MAX_HEIGHT, 10); setHeight(height); setShowMore(show); if (ifr.current && show) { ifr.current.remove(); } }, []); useEffect(() => { if (!ref.current || !ifr.current?.contentWindow) return; ifr.current.contentWindow.onresize = onresize; onresize(); }, [details]); if (!details) returnnull; return ( <section className="section detail-content"> <div className="content-wrapper"> <div className="content" dangerouslySetInnerHTML={{ __html: details }} style={{ maxHeight }} ref={ref} /> {/* 這個iframe是用來動態(tài)監(jiān)聽content高度的變化的 */} <iframe title={IFRAME_ID} id={IFRAME_ID} ref={ifr} /> </div> {isFolded && showMore && ( <> <div className="show-more" onClick={() => { setMaxHeight(isFolded ? "none" : MAX_HEIGHT); }} > 查看全部 <IconArrowDown className="icon" /> </div> <div className="mask" /> </> )} </section> ); };
這種方式實際上就是對 ResizeObserver 的一種 hack,經(jīng)過多次實踐,符合功能要求。
總結:
解決問題要盡可能的考慮多種情況,對比多種方案,采取最為可靠的一種方案。
注: 監(jiān)聽 DOM 元素的高度變化,可以采用內(nèi)嵌 iframe 的方式來解決。
到此這篇關于JavaScript如何動態(tài)監(jiān)聽DOM元素高度詳解的文章就介紹到這了,更多相關JS動態(tài)監(jiān)聽DOM 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
27個JavaScript數(shù)組常見方法匯總與實例說明
這篇文章主要介紹了JavaScript數(shù)組常見方法匯總與實例說明包括數(shù)組修改,數(shù)組增加,數(shù)組遍歷,數(shù)組排序等操作,需要的朋友可以參考下2022-12-12JS中實現(xiàn)replaceAll的方法(實例代碼)
本文是對JS中實現(xiàn)replaceAll的方法進行了詳細的總結介紹,需要的朋友可以過來參考下,希望對大家有所幫助2013-11-11document.documentElement的一些使用技巧
documentElement 屬性可返回文檔的根節(jié)點,接下來為大家詳細介紹下document.documentElement的一些使用技巧,感興趣的朋友可以參考下哈2013-04-04