CSS 动画监听元素渲染
在数据驱动视图的框架下,你最头疼的事情是什么?没错,就是获取dom
。大部分业务逻辑都可以在数据层面进行处理,但有些情况就不得不去获取真实的dom
,比如获取元素的宽高:
dom.offsetHeight
或者调用某些dom
方法等
dom.scrollTop = 100
通常在框架里,比如说vue
中,会如何获取真实 dom
呢?我想大家可能都用过这样一个方法nextTick
,用于在数据更新后获取 dom
,如下:
this.show = true this.$nextTick(() => ( document.getElementById('xx').scrollTop = 100 ))
用过的都知道,这个方式非常不靠谱,经常会出现诸如类似这样的错误:
Cannot read property 'scrollTo' of undefined
碰到这种情况,很多同学可能会用定时器,如果500
不行,那就换1000
,只要延时够长,总能获取到真实dom
的。
this.show = true settimeout(() => ( document.getElementById('xx').scrollTop = 0 ), 500)
或许这些框架底层有其他解决方式,不过我并不精通这些,那么,从原生角度,有什么比较好的方式去解决这些问题呢?换句话说,如何确保元素渲染时机呢?
一、如何监听元素渲染?
元素监听最官方的方式是MutationObserver
查看,这个API
天生就是为了 dom
变化检测而生的。
功能非常强大,几乎能监听到 dom
的所有变化,包括上面提到的元素渲染成功。
但是,正是因为过于强大,所以它的api
就变得极其繁琐,下面是MDN
里的一段例子:
// 选择需要观察变动的节点 const targetNode = document.getElementById("some-id"); // 观察器的配置(需要观察什么变动) const config = { attributes: true, childList: true, subtree: true }; // 当观察到变动时执行的回调函数 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."); } } }; // 创建一个观察器实例并传入回调函数 const observer = new MutationObserver(callback); // 以上述配置开始观察目标节点 observer.observe(targetNode, config); // 之后,可停止观察 observer.disconnect();
我相信,除非特殊需求,没人会愿意写上这样一堆代码吧,定时器不比这个“香”多了?
那么,有没有一些简洁的、靠谱的监听方法呢?
其实,文章标题已经暴露了,没错,我们可以用 CSS 动画来监听元素渲染。
原理其实很简单,给元素一个动画,动画会在元素添加到页面时自动播放,进而触发animation*
相关事件。
代码也很简单,先定义一个无关紧要的 CSS 动画,不能影响视觉效果,比如:
@keyframes appear{ to { opacity: .99; } }
然后给需要监听的元素上添加这个动画:
div{ animation: appear .1s; }
最后,只需要在这个元素或者及其父级上监听动画开始时机就行了,如果有多个元素,建议放在共同父级上。
parent.addEventListener('animationstart', (ev) => { if (ev.animationName == 'appear') { // 元素出现了,可以获取 dom 信息了 } })
下面来看几个实际例子
二、多行文本展开收起
这个例子,用 CSS
容器虽然可以实现效果,但是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
动画来监听,一切就都好办了。
我们先回到最基础的HTML
结构:
<div class="text-wrap"> <div class="text-content"> <label class="expand"><input type="checkbox" hidden></label> 欢迎关注码云笔记,这里有一些有趣的、你可能不知道的 HTML、CSS、JS 小技巧技巧。 </div> </div>
这些结构是为了实现右下角的“展开”按钮必不可少的。
相关 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: '收起'; }
然后,我们给文本容器添加一个无关紧要的动画:
.text-content{ /**/ animation: appear .1s; } @keyframes appear { to { opacity: .99; } }
然后,我们在父级上监听这个动画,我这里直接监听document
,这里做的事情很简单,判断一下容器的滚动高度和实际高度,如果滚动高度超过实际高度,说明文本较多,超出了指定行数,这种情况就给容器添加一个特殊的属性:
document.addEventListener('animationstart', (ev) => { if (ev.animationName == 'appear') { ev.target.dataset.mul = ev.target.scrollHeight > ev.target.offsetHeight; } })
然后根据这个属性,判断“展开”按钮隐藏或者显示:
.expand{ /**/ visibility: hidden; } .text-content[data-mul="true"] .expand{ visibility: visible; }
这样只有在文本较多时,“展开”按钮才会出现,效果如下:
三、文本超长时自动滚动
再来看一个例子,相信大家都碰到过。
先看效果吧,就是一个无限滚动的效果,类似与以前的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%); } }
这样实现会有两个问题,效果如下
一是较少的文本也发生的滚动,二是滚动速度不一致。
所以,有必要借助 JS 来修正一下。
还是上面的方式,我们直接用 CSS 动画来监听元素渲染
.marqee{ /**/ animation: appear .1s; } @keyframes appear { to { opacity: .99; } }
然后监听动画开始事件,这里要做两件事,也就是为了修正前面提到的两个问题,一个是判断文本的真实宽度和容器宽度的关系,还有一个是获取判断文本宽度和容器宽度的比例关系,因为文本越长,需要滚动的时间也越长
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); } })
拿到这些状态后,我们改一下前面的动画。
只有data-mul
为true
的情况下,才执行动画,并且动画时长是和--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
然后,根据这个id
自动锚定到这个任务上(让这个任务滚动到屏幕中间)
由于这个任务是通过接口返回渲染的,所以必须等待 dom
渲染完全才能获取到。
传统的方式可能又要通过定时器了,这时可以考虑用动画监听的方式。
.item{ /**/ animation: appear .1s; } @keyframes appear { to { opacity: .99; } }
然后我们只需要监听动画开始事件,判断一下元素的 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
中也可以将这个监听直接绑定在父级模板上,这样会更方便
<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')) { // } })
这个需要额外注意一下
六、总结一下
是不是从来没有用过这些方式,赶紧试一试吧,相信会有不一样的感受,下面总结一下
- 在数据驱动视图的框架下,获取
dom
是一件比较头疼的事情 - 很多时候数据更新了,
dom
还没来得及更新,这时获取就出错了 - 元素监听最官方的方式是
MutationObserver
,但是比较复杂,一般情况下不会有人用 - 另辟蹊径,我们可以用 CSS 动画来监听元素渲染
- 原理非常简单,给元素一个动画,动画会在元素添加到页面时自动播放,进而触发
animation*
相关事件 - 利用这个技巧,我们可以很轻松的获取元素的
dom
相关信息已经触发相关事件 - 注意一下框架里的编译,可能会更改动画名称
声明:本文转载,原文链接
码云笔记 » CSS 动画监听元素渲染