Collapse组件在做内容折叠与展开显示的时候,还是用到很多的。这一个组件的内容相对于Badge和Tag组件更多一点,所以打算分成三篇文章来讲。
高度不固定的css动画
第一篇先来讲一讲对于高度不确定的元素,怎么做高度的展开动画效果。
看一下大佬对transition为什么不能对height:auto实现过渡动画
的解释:
这里贴一个css trick上对于这种问题的解决方案:Using CSS Transitions on Auto Dimensions
说一下在element-ui中的实现思路吧:
- 通过transition的钩子函数 + render渲染函数,我们来自定义一个过渡效果,由JS来掌控变化的值
- 展开时,height从
0 逐步增大到 scrollHeight
- 收缩时,height从
scrollHeight 逐步减小到 0
过渡动画最终实现效果
https://jsfiddle.net/huang_ju...
scrollHeight, paddingSize
通过自适应文本高度的输入框那篇文章,我们已经了解到scrollHeight的高度是包含了padding的,所以在这里的动画时,需要对padding-top,padding-bottom,height三个属性都做变化。
其次,对于展开时高度的值,需要scrollHeight - paddingSize
let Transition = {
beforeEnter(el){
if(!el.dataset) el.dataset = {};
let styles = window.getComputedStyle(el);
// 记录展开前的属性值
el.dataset.oldOverflow = styles.getPropertyValue('overflow');
el.dataset.oldPaddingTop = styles.getPropertyValue('padding-top');
el.dataset.oldPaddingBottom = styles.getPropertyValue('padding-bottom');
// 这三个都为0,scrollHeight的高度就是真实的内容高度了
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
el.classList.add('collapse-transition');
el.style.overflow = 'hidden';
},
enter(el) {
if(el.scrollHeight !== 0) {
// 动画过程中,逐渐增大到展开前应占的高度值
el.style.height = el.scrollHeight + 'px';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
},
afterEnter(el) {
el.classList.remove('collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
}
// ....
}
重绘与重排
观察jsfiddle的demo示例代码,会发现这句
let Transition = {
// ...
leave(el) {
if (el.scrollHeight !== 0) {
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
}
// ...
}
为什么要加el.scrollHeight !== 0的判断呢?
试一下,如果不加这个判断,直接变化height,paddingTop,paddingBottom的值到0,这个时候,收缩时并不会有过渡动画,元素马上就消失了。
我们可以替换一下上面的代码
let Transition = {
// ...
leave(el) {
setTimeout(() => {
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}, 20)
}
// ...
}
这时候,收缩也会有过渡动画。但当我们尝试将延迟时间改为0的时候,并无效,延迟时间需要大于0才会有过渡效果。所以,这个过渡动画似乎和执行队列就没什么关联了。
接下来,我们把目标锁定在了重绘和重排上。
着重看一下大佬的这篇文章:高性能JavaScript 重排与重绘
1.什么是重排和重绘
浏览器解析页面生成DOM树
和渲染树
,DOM树表示节点结构,渲染树表示节点如何显示。
DOM树节点的属性发生变化时,浏览器可能会重新计算去绘制渲染树,这个过程叫重排
,渲染树映射到屏幕上显示的过程就叫重绘
。
2.重排什么时候发生每次重排,必然会导致重绘,那么,重排会在哪些情况下发生?
- 添加或者删除可见的DOM元素
- 元素位置改变
- 元素尺寸改变
- 元素内容改变(例如:一个文本被另一个不同尺寸的图片替代)
- 页面渲染初始化(这个无法避免)
- 浏览器窗口尺寸改变
可以总结为,当一个节点的位置、大小、内容发生改变,或者增删节点的情况下会发生重排。
3.渲染树变化的排队和刷新浏览器会把多次对节点的修改“保存”起来(大多数浏览器通过队列化修改并批量执行来优化重排过程),最终一次完成!但是,有些时候你可能会(经常是不知不觉)强制刷新队列并要求计划任务立即执行
。获取布局信息的操作会导致队列刷新,比如:
- offsetTop, offsetLeft, offsetWidth, offsetHeight(节点位置)
- scrollTop, scrollLeft, scrollWidth, scrollHeight(节点位置、大小)
- clientTop, clientLeft, clientWidth, clientHeight(节点大小)
- getComputedStyle() (currentStyle in IE)(节点样式)
可以这么理解,要获取这些值,浏览器就需要把前面缓存的重排操作先给执行了,才能计算最新的,正确的节点信息。而这种中断,打断了浏览器自身对于重排的优化,是需要我们避免的。
4.最小化重排和重绘
虽然浏览器对重排进行了优化,但我们不经意的操作会打断这种优化。所以,修改样式信息时,尽量集中操作
5.fragment元素的应用前面说了,重排大多数都是由于节点位置、大小,增删造成的。节点位置,大小的改变我们可以通过集中操作来规范以优化性能。而对于节点的增删就可以通过fragment元素,来避免频繁的重排和重绘了
。
尽量不要在布局信息改变时做查询(会导致渲染队列强制刷新)
同一个DOM的多个属性改变可以写在一起(减少DOM访问,同时把强制渲染队列刷新的风险降为0)
如果要批量添加DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排(fragment元素的应用)
我们再看一下出问题的这段代码,如果不加scrollHeight的时候:
let Transition = {
// ...
beforeLeave(el) {
// ...
el.style.height = el.scrollHeight - parseFloat(el.dataset.oldPaddingTop) - parseFloat(el.dataset.oldPaddingBottom) + 'px';
el.style.overflow = 'hidden';
el.classList.add('collapse-transition');
//var tmp = el.offsetTop;
},
leave(el) {
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
},
afterLeave(el) {
//...
}
}
我们进行了一系列操作,浏览器缓存了这几次的重排。等到主线程结束,开始渲染的时候,直接就往下一直渲染到了height:0;这句。所以就没有了过渡动画。
而加上大于零的定时器,应该是由于,1.先运行js主线程。2.执行重排重绘。3.执行定时回调,变化height的值
可以试下,讲注释的这行代码(var tmp = el.offsetTop;)加入,也有过渡动画效果。
结合上面对重排和重绘的学习,可以很容易的理解到:这一句代码强制刷新了重排的队列。让前面设置的属性,增加的过渡效果的类名。让它先渲染好,然后运行到leave函数的时候,再变化高度值,transition就起作用了。
总结
通过过渡效果的实现:
- 学到了一种新的思路,如果利用render+transition钩子函数,自己生成一个JS掌控的transition
- 重排与重绘对于性能的影响,以及编程中需要注意的点
参考文章:
1.Using CSS Transitions on Auto Dimensions
2.css3怎么实现高度从固定到自动的过渡动画?
3.高性能JavaScript 重排与重绘
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。