在數(shù)據(jù)驅動視圖的框架下,你最頭疼的事情是什么?沒錯,就是獲取dom。大部分業(yè)務邏輯都可以在數(shù)據(jù)層面進行處理,但有些情況就不得不去獲取真實的dom,比如獲取元素的寬高
dom.offsetHeight
或者調用某些dom方法等
dom.scrollTop = 100
通常在框架里,比如說vue中,會如何獲取真實 dom 呢?我想大家可能都用過這樣一個方法nextTick,用于在數(shù)據(jù)更新后獲取 dom,如下
this.show = truethis.$nextTick(() => ( document.getElementById('xx').scrollTop = 100))
用過的都知道,這個方式非常不靠譜,經常會出現(xiàn)諸如類似這樣的錯誤
Cannot read property 'scrollTo' of undefined
碰到這種情況,很多同學可能會用定時器,如果500不行,那就換1000,只要延時夠長,總能獲取到真實dom的。
this.show = truesettimeout(() => ( document.getElementById('xx').scrollTop = 0),500)
或許這些框架底層有其他解決方式,不過我并不精通這些,那么,從原生角度,有什么比較好的方式去解決這些問題呢?換句話說,如何確保元素渲染時機呢?
元素監(jiān)聽最官方的方式是MutationObserver,這個API天生就是為了 dom變化檢測而生的。
https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
功能非常強大,幾乎能監(jiān)聽到 dom的所有變化,包括上面提到的元素渲染成功。
但是,正是因為過于強大,所以它的api就變得極其繁瑣,下面是MDN里的一段例子
// 選擇需要觀察變動的節(jié)點const targetNode = document.getElementById("some-id");// 觀察器的配置(需要觀察什么變動)const config = { attributes: true, childList: true, subtree: true };// 當觀察到變動時執(zhí)行的回調函數(shù)const callback = function (mutationsList, observer) { // Use traditional 'for loops' for IE 11 for (let mutation of mutationsList) { if (mutation.type === "childList") { console.log("A child node has been added or removed."); } else if (mutation.type === "attributes") { console.log("The " + mutation.attributeName + " attribute was modified."); } }};// 創(chuàng)建一個觀察器實例并傳入回調函數(shù)const observer = new MutationObserver(callback);// 以上述配置開始觀察目標節(jié)點observer.observe(targetNode, config);// 之后,可停止觀察observer.disconnect();
我相信,除非特殊需求,沒人會愿意寫上這樣一堆代碼吧,定時器不比這個“香”多了?
那么,有沒有一些簡潔的、靠譜的監(jiān)聽方法呢?
其實,文章標題已經暴露了,沒錯,我們可以用 CSS 動畫來監(jiān)聽元素渲染。
原理其實很簡單,給元素一個動畫,動畫會在元素添加到頁面時自動播放,進而觸發(fā)animation*相關事件。
圖片
代碼也很簡單,先定義一個無關緊要的 CSS 動畫,不能影響視覺效果,比如
@keyframes appear{ to { opacity: .99; }}
然后給需要監(jiān)聽的元素上添加這個動畫
div{ animation: appear .1s;}
最后,只需要在這個元素或者及其父級上監(jiān)聽動畫開始時機就行了,如果有多個元素,建議放在共同父級上
parent.addEventListener('animationstart', (ev) => { if (ev.animationName == 'appear') { // 元素出現(xiàn)了,可以獲取dom信息了 }})
下面來看幾個實際例子
沒錯,又是這個例子。
前不久,嘗試用 CSS 容器實現(xiàn)了這個效果,有興趣的可以參考這篇文章:
嘗試借助CSS @container實現(xiàn)多行文本展開收起
雖然最后實現(xiàn)了,但是dom結構及其復雜,如下
<div class="text-wrap"> <div class="text" title="歡迎關注前端偵探,這里有一些有趣的、你可能不知道的HTML、CSS、JS小技巧技巧。"> <div class="text-size"> <div class="text-flex"> <div class="text-content"> <label class="expand"><input type="checkbox" hidden></label> 歡迎關注前端偵探,這里有一些有趣的、你可能不知道的HTML、CSS、JS小技巧技巧。 </div> </div> </div> </div> <div class="text-content text-place"> 歡迎關注前端偵探,這里有一些有趣的、你可能不知道的HTML、CSS、JS小技巧技巧。 </div></div>
很多重復的文本和多余的標簽,這些都是為了配合容器查詢添加的。
其實說到底,只是為了判斷一下尺寸,其實 JS 是更好的選擇,麻煩的只是獲取尺寸的時機。如果通過 CSS 動畫來監(jiān)聽,一切就都好辦了。
我們先回到最基礎的HTML結構
<div class="text-wrap"> <div class="text-content"> <label class="expand"><input type="checkbox" hidden></label> 歡迎關注前端偵探,這里有一些有趣的、你可能不知道的HTML、CSS、JS小技巧技巧。 </div></div>
這些結構是為了實現(xiàn)右下角的“展開”按鈕必不可少的,如果不太清楚是如何布局的,可以回顧一下之前這篇文章:
CSS 實現(xiàn)多行文本“展開收起”
相關 CSS 如下
.text-wrap{ display: flex; position: relative; width: 300px; padding: 8px; outline: 1px dashed #9747FF; border-radius: 4px; line-height: 1.5; text-align: justify; font-family: cursive;}.expand{ font-size: 80%; padding: .2em .5em; background-color: #9747FF; color: #fff; border-radius: 4px; cursor: pointer; float: right; clear: both;}.expand::after{ content: '展開';}.text-content{ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;}.text-content::before{ content: ''; float: right; height: calc(100% - 24px);}.text-wrap:has(:checked) .text-content{ -webkit-line-clamp: 999;}.text-wrap:has(:checked) .expand::after{ content: '收起';}
效果如下
通過前一節(jié)的原理,我們給文本容器添加一個無關緊要的動畫
.text-content{ /**/ animation: appear .1s;}@keyframes appear { to { opacity: .99; }}
然后,我們在父級上監(jiān)聽這個動畫,我這里直接監(jiān)聽document,這里做的事情很簡單,判斷一下容器的滾動高度和實際高度,如果滾動高度超過實際高度,說明文本較多,超出了指定行數(shù),這種情況就給容器添加一個特殊的屬性
document.addEventListener('animationstart', (ev) => { if (ev.animationName == 'appear') { ev.target.dataset.mul = ev.target.scrollHeight > ev.target.offsetHeight; }})
然后根據(jù)這個屬性,判斷“展開”按鈕隱藏或者顯示
.expand{ /**/ visibility: hidden;}.text-content[data-mul="true"] .expand{ visibility: visible;}
這樣只有在文本較多時,“展開”按鈕才會出現(xiàn),效果如下
圖片
是不是要簡單很多?完整代碼可以參考以下鏈接
再來看一個例子,相信大家都碰到過。
先看效果吧,就是一個無限滾動的效果,類似與以前的marquee標簽
首先來看HTML,并沒有什么特別之處
<div class="marqee"> <span class="text" title="這是一段可以自動滾動的文本">這是一段可以自動滾動的文本</span></div>
這里是首尾無縫銜接,所以需要兩份文本,我這里用偽元素生成
.text::after{ content: attr(title); padding: 0 20px;}
單純的滾動其實很容易,就一行 CSS,如下
.text{ animation: move 2s linear infinite;}@keyframes move{ to { transform: translateX(-50%); }}
這樣實現(xiàn)會有兩個問題,效果如下
圖片
一是較少的文本也發(fā)生的滾動,二是滾動速度不一致。
所以,有必要借助 JS來修正一下。
還是上面的方式,我們直接用CSS動畫來監(jiān)聽元素渲染
.marqee{ /**/ animation: appear .1s;}@keyframes appear { to { opacity: .99; }}
然后監(jiān)聽動畫開始事件,這里要做兩件事,也就是為了修正前面提到的兩個問題,一個是判斷文本的真實寬度和容器寬度的關系,還有一個是獲取判斷文本寬度和容器寬度的比例關系,因為文本越長,需要滾動的時間也越長
document.addEventListener('animationstart', (ev) => { if (ev.animationName == 'appear') { ev.target.dataset.mul = ev.target.scrollWidth > ev.target.offsetWidth; ev.target.style.setProperty('--speed', ev.target.scrollWidth / ev.target.offsetWidth); }})
拿到這些狀態(tài)后,我們改一下前面的動畫。
只有data-mul為true的情況下,才執(zhí)行動畫,并且動畫時長是和--speed成比例的,這樣可以保證所有文本的速度是一致的
.marqee[data-mul="true"] .text{ display: inline-block; animation: move calc(var(--speed) * 3s) linear infinite;}
還有就是只有data-mul為true的情況下才會生成雙份文本
.marqee[data-mul="true"] .text::after{ content: attr(title); padding: 0 20px;}
這樣判斷以后,就能得到我們想要的效果了
完整代碼可以參考以下鏈接
最后再來一個例子,其實這個方式我平時用的很多了,一個任務列表頁面,我們有時候會遇到這樣的需求,在地址欄上傳入一個 id,例如
https://xxx.com?id=5
然后,根據(jù)這個id自動錨定到這個任務上(讓這個任務滾動到屏幕中間)
由于這個任務是通過接口返回渲染的,所以必須等待 dom渲染完全才能獲取到。
圖片
傳統(tǒng)的方式可能又要通過定時器了,這時可以考慮用動畫監(jiān)聽的方式。
.item{ /**/ animation: appear .1s;}@keyframes appear { to { opacity: .99; }}
然后我們只需要監(jiān)聽動畫開始事件,判斷一下元素的 id 是否和我們傳入的一致,如果是一致就直接錨定就行了
const current_id = 'item_5';// 假設這個是url傳進來的document.addEventListener('animationstart', (ev) => { if (ev.animationName == 'appear' && ev.target.id === current_id) { ev.target.scrollIntoView({ block: 'center' }) }})
這樣就能準確無誤的獲取到錨定元素并且滾動定位了,效果如下
完整代碼可以參考以下鏈接
在實際使用中,有一些要注意一下。
比如,在vue中也可以將這個監(jiān)聽直接綁定在父級模板上,這樣會更方便
<div @animationstart="apear"> </div>
還有一點比較重要,很多時候我們用的的可能是CSS scoped,比如
<style scoped>.item{ /**/ animation: appear .1s;}@keyframes appear { to { opacity: .99; }}</style>
如果是這種寫法就需要注意了,因為在編譯過程中,這個動畫名稱會加一些哈希后綴,類似于這樣
所以,我們在animationstart判斷時要改動一下,比如用startsWith
document.addEventListener('animationstart', (ev) => { if (ev.animationName.startsWith('appear')) { // }})
這個需要額外注意一下
是不是從來沒有用過這些方式,趕緊試一試吧,相信會有不一樣的感受,下面總結一下
總的來說,這是一個非常實用的小技巧,雖然沒有純 CSS那么“高級”,但是卻是最“實用”的。
[1]CSS els with animation (juejin.cn): https://code.juejin.cn/pen/7323120296334983187
[2]CSS els with animation (codepen.io): https://codepen.io/xboxyan/pen/gOELbxV
[3]CSS marquee width animation (juejin.cn): https://code.juejin.cn/pen/7323125690973945897
[4]CSS marquee width animation (codepen.io): https://codepen.io/xboxyan/pen/YzgGmLb
[5]CSS scrollIntoView with animation (juejin.cn): https://code.juejin.cn/pen/7323419904693469234
[6]CSS scrollIntoView with animation (codepen.io): https://code.juejin.cn/pen/7323419904693469234
本文鏈接:http://www.www897cc.com/showinfo-26-61908-0.htmldom 獲取不到?試試 CSS 動畫監(jiān)聽元素渲染吧
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
下一篇: 未來世界的12個軟件開發(fā)預測