dom 获取不到?试试 CSS 动画监听元素渲染吧

why前端

共 12650字,需浏览 26分钟

 · 2024-04-10

在数据驱动视图的框架下,你最头疼的事情是什么?没错,就是获取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变化检测而生的。

https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver

功能非常强大,几乎能监听到 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*相关事件。

68e5916a56616406456a7a9025c71579.webp

代码也很简单,先定义一个无关紧要的 CSS 动画,不能影响视觉效果,比如

        
          
          
        
          @keyframes appear{
to {
opacity: .99;
}
}

然后给需要监听的元素上添加这个动画

        
          
          
        
          div{
animation: appear .1s;
}

最后,只需要在这个元素或者及其父级上监听动画开始时机就行了,如果有多个元素,建议放在共同父级上

        
          
          
        
          parent.addEventListener('animationstart', (ev) => {
if (ev.animationName == 'appear') {
// 元素出现了,可以获取dom信息了
}
})

下面来看几个实际例子

二、多行文本展开收起

没错,又是这个例子。

前不久,尝试用 CSS 容器实现了这个效果,有兴趣的可以参考这篇文章:

尝试借助CSS @container实现多行文本展开收起

虽然最后实现了,但是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 实现多行文本“展开收起”

相关 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: '收起';
}

效果如下

a13750988803d01bb98e47e1db391d77.webp

通过前一节的原理,我们给文本容器添加一个无关紧要的动画

        
          
          
        
          .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;
}

这样只有在文本较多时,“展开”按钮才会出现,效果如下

faaa2687b65e520c682fe78e4c606b5f.webp

是不是要简单很多?完整代码可以参考以下链接

  • CSS els with animation (juejin.cn)[1]

  • CSS els with animation (codepen.io)[2]

三、文本超长时自动滚动

再来看一个例子,相信大家都碰到过。

先看效果吧,就是一个无限滚动的效果,类似与以前的marquee标签

2bd567b3216c1833293fda53f2813b3f.webp

首先来看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%);
}
}

这样实现会有两个问题,效果如下

8fb713ad7b7a78686e31512ea3019559.webp

一是较少的文本也发生的滚动,二是滚动速度不一致。

所以,有必要借助 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-multrue的情况下,才执行动画,并且动画时长是和--speed成比例的,这样可以保证所有文本的速度是一致的

        
          
          
        
          .marqee[data-mul="true"] .text{
display: inline-block;
animation: move calc(var(--speed) * 3s) linear infinite;
}

还有就是只有data-multrue的情况下才会生成双份文本

        
          
          
        
          .marqee[data-mul="true"] .text::after{
content: attr(title);
padding: 0 20px;
}

这样判断以后,就能得到我们想要的效果了

2bd567b3216c1833293fda53f2813b3f.webp

完整代码可以参考以下链接

  • CSS marquee width animation (juejin.cn)[3]

  • CSS marquee width animation (codepen.io)[4]

四、元素锚定定位

最后再来一个例子,其实这个方式我平时用的很多了,一个任务列表页面,我们有时候会遇到这样的需求,在地址栏上传入一个 id,例如

        
          
          
        
          https://xxx.com?id=5

然后,根据这个id自动锚定到这个任务上(让这个任务滚动到屏幕中间)

由于这个任务是通过接口返回渲染的,所以必须等待 dom渲染完全才能获取到。

9140af1f0ea8bbdda93d0878c07b32e5.webp

传统的方式可能又要通过定时器了,这时可以考虑用动画监听的方式。

        
          
          
        
          .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'
})
}
})

这样就能准确无误的获取到锚定元素并且滚动定位了,效果如下

fe28a6bd341ce45f409465ed7286bc1a.webp

完整代码可以参考以下链接

  • CSS scrollIntoView with animation (juejin.cn)[5]

  • CSS scrollIntoView with animation (codepen.io)[6]

五、其他注意事项

在实际使用中,有一些要注意一下。

比如,在vue中也可以将这个监听直接绑定在父级模板上,这样会更方便

        
          
          
        
          
            <div @animationstart="apear">
            

</div>

还有一点比较重要,很多时候我们用的的可能是CSS scoped,比如

        
          
          
        
          
            <style scoped>
            
.item{
/**/
animation: appear .1s;
}
@keyframes appear {
to {
opacity: .99;
}
}
</style>

如果是这种写法就需要注意了,因为在编译过程中,这个动画名称会加一些哈希后缀,类似于这样

19df3210f18faa0d0f94fa2e01705214.webp

所以,我们在animationstart判断时要改动一下,比如用startsWith

        
          
          
        
          document.addEventListener('animationstart', (ev) => {
if (ev.animationName.startsWith('appear')) {
//
}
})

这个需要额外注意一下

六、总结一下

是不是从来没有用过这些方式,赶紧试一试吧,相信会有不一样的感受,下面总结一下

  1. 在数据驱动视图的框架下,获取dom是一件比较头疼的事情

  2. 很多时候数据更新了,dom还没来得及更新,这时获取就出错了

  3. 元素监听最官方的方式是MutationObserver,但是比较复杂,一般情况下不会有人用

  4. 另辟蹊径,我们可以用 CSS 动画来监听元素渲染

  5. 原理非常简单,给元素一个动画,动画会在元素添加到页面时自动播放,进而触发animation*相关事件

  6. 利用这个技巧,我们可以很轻松的获取元素的dom相关信息已经触发相关事件

  7. 注意一下框架里的编译,可能会更改动画名称

总的来说,这是一个非常实用的小技巧,虽然没有纯 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

浏览 4
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报