1.回顾涉及到的基础
[Vue深入浅出]知晓Vue中的render函数
2.element-ui中的collapse-transition.js
源码:
import { addClass, removeClass } from 'element-ui/src/utils/dom';
class Transition {
beforeEnter(el) {
addClass(el, 'collapse-transition');
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.style.height = '0';
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
enter(el) {
el.dataset.oldOverflow = el.style.overflow;
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + 'px';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
} else {
el.style.height = '';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
el.style.overflow = 'hidden';
}
afterEnter(el) {
// for safari: remove class then reset height is necessary
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
}
beforeLeave(el) {
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.dataset.oldOverflow = el.style.overflow;
el.style.height = el.scrollHeight + 'px';
el.style.overflow = 'hidden';
}
leave(el) {
if (el.scrollHeight !== 0) {
// for safari: add class after set height, or it will jump to zero height suddenly, weired
addClass(el, 'collapse-transition');
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
}
afterLeave(el) {
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
}
export default {
name: 'ElCollapseTransition',
functional: true,
render(h, { children }) {
const data = {
on: new Transition()
};
return h('transition', data, children);
}
};
场景:
在el-tree,el-menu,el-collapse中都有用过上述这个组件。例如:
el-tree:
el-menu:
el-collapse:
解释:
先从源码最后的export default那部分代码进行分析。从name: 'ElCollapseTransition'和functional: true可以知道这是一个函数式组件。
然后分析render函数,render传入的h就是createElement函数,children是从context(就是vnode.context,也就是实例中的this)中解构出来的,相当于已被实例化的components中注册的组件。
然后h的传入参数,第一个参数是要生成的标签或者组件的名称,这里传入transition也就是要生成transition组件。第二个参数为数据对象,这里实例化源码上部分声明的Transition类,类里面定义好过渡动画时的钩子函数(beforeEnter,enter,afterEnter,beforeLeave,leave,afterLeave)。后面把这个Transition类的实例放在on事件监听属性里,效果就跟下面的函数一样:
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
>
<!-- ... -->
</transition>
接下来再看钩子函数里的内容,先看beforeEnter,enter,afterEnter。执行顺序依次为beforeEnter>enter>afterEnter:
beforeEnter(el) {
addClass(el, 'collapse-transition');
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.style.height = '0';
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
enter(el) {
el.dataset.oldOverflow = el.style.overflow;
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + 'px';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
} else {
el.style.height = '';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
el.style.overflow = 'hidden';
}
afterEnter(el) {
// for safari: remove class then reset height is necessary
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
}
在beforeEnter中,一开始添加collapse-transition的class。类的定义如下:
// packages\theme-chalk\src\common\transition.scss
.collapse-transition {
transition: 0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out;
}
由.collapse-transition可知,整个ElCollapseTransition的展示原理就是让**height**,**padding-top**,**padding-bottom**从0到设定值的过程,加上**transition**动画过渡。
回到beforeEnter函数中继续分析,先初始化dataset用于存放html节点的data-*属性。然后把style里的paddingTop和paddingBottom的数据暂存到dataset中,然后把height,paddingTop,paddingBottom都设置为0。
在enter函数中,通过el.scrollHeight获取元素的正文全文高度,然后赋值到el.style.height中。如果el.scrollHeight为0,则代表el没有子元素,代表其高度不是靠子元素撑起来的,可能是在css中定义好height的,此时通过el.style.height = '',让元素高度计算取值恢复到css的class中定义的高度。
然后通过el.style.paddingTop = el.dataset.oldPaddingTop; el.style.paddingBottom = el.dataset.oldPaddingBottom;恢复paddingTop和paddingBottom初始值。
最后afterEnter函数中,移除collapse-transition的class,然后el.style.height = ''以恢复设定值。
之后的 beforeLeave,leave,afterLeave同理。
拓展:
clientHeight,offsetHeight,scrollHeight的区别:
clientHeight:包括padding但不包括border、margin的元素的页面可视高度,有滚动条时计算统计滚动条高度。对于inline的元素这个属性一直是0,单位px,只读。如下图所示:
offsetHeight:和clientHeight的区别是统计高度时还包括border。如下图所示:
scrollHeight:和clientHeight的区别是,无论有没有滚动条,统计的都是元素正文高度,如下图所示:
3.collapse-transition存在的问题
举一个场景,先放出代码:
<template lang="pug">
.content
el-button(@click="visible=!visible") {{visible?'显示':'隐藏'}}
el-collapse-transition
.box1(v-if="visible")
.box2
</template>
<script>
export default {
data () {
return {
visible: false
}
}
}
</script>
<style lang="scss" scoped>
.box1 {
position: relative;
width: 200px;
height: 200px;
overflow: auto;
}
.box2 {
width: 100%;
height: 400px;
background-color: red;
}
</style>
在el-collapse-transition里面存在一个box1,box1里面也存在一个box2。box1的高度为200px,比box2的高度400px要小。在动画效果如下所示:
总结特征:
- 展开时,先顺畅展开到box2的高度,动画结束时变成box1的高度
- 隐藏式,高度先突然从box1变回box2的高度,后再顺场地变为0
原因分析:
这里直接贴涉及到的代码和注释:
enter(el) {
el.dataset.oldOverflow = el.style.overflow;
if (el.scrollHeight !== 0) {
// enter状态时,el.style.height直接取scrollHeight的高度,即box2的高度400px
el.style.height = el.scrollHeight + 'px';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
} else {
el.style.height = '';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
el.style.overflow = 'hidden';
}
afterEnter(el) {
removeClass(el, 'collapse-transition');
// afterEnter状态时,el.style.height设置为'',此时高度变回el在.box1类中设置的高度200px
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
}
beforeLeave(el) {
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.dataset.oldOverflow = el.style.overflow;
// el.style.height直接取scrollHeight的高度,即box2的高度400px
el.style.height = el.scrollHeight + 'px';
el.style.overflow = 'hidden';
}
leave(el) {
if (el.scrollHeight !== 0) {
// for safari: add class after set height, or it will jump to zero height suddenly, weired
addClass(el, 'collapse-transition');
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
}
如何避免这类情况:
在原代码上进行修改:
<template lang="pug">
.ts-yard
el-button(@click="visible=!visible") {{visible?'隐藏':'显示'}}
el-collapse-transition
// 用一个普通的div包裹着.box1,且把v-if判断条件移到该div上,其他代码不变
.box(v-if="visible")
.box1
.box2
</template>
<script>
// 和之前一样...
</script>
<style lang="scss" scoped>
// 和之前一样...
</style>
用一个div包裹要展示的内容主要是为了让el.scrollHeight得出的高度和el自身高度一样。
最后显示更改后的效果: