[element-ui源码]element-ui的collapse-transition到底写了什么?

3,113 阅读4分钟

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类,类里面定义好过渡动画时的钩子函数(beforeEnterenterafterEnterbeforeLeaveleaveafterLeave)。后面把这个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>

接下来再看钩子函数里的内容,先看beforeEnterenterafterEnter。执行顺序依次为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里的paddingToppaddingBottom的数据暂存到dataset中,然后把heightpaddingToppaddingBottom都设置为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;恢复paddingToppaddingBottom初始值。

最后afterEnter函数中,移除collapse-transition的class,然后el.style.height = ''以恢复设定值。

之后的 beforeLeaveleaveafterLeave同理。

拓展:

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自身高度一样。

最后显示更改后的效果: