Element源码分析系列9-Switch(开关)

3,608 阅读4分钟

简介

终于遇到一个简单的组件了,不过这个组件的实现还是和我之前的实现有所不同,下图Element的Switch组件

看着就很简单,是不是呀,官网代码点此

之前自己的实现方式

关于开关组件,之前自己写了一个,其实这个组件是不需要绑定任何click事件的,也就是说js部分几乎可以不写,核心思想就是利用<input type=checkbox>的checked属性,当鼠标点击input时,会切换其checked属性,这是原生checkbox的特性。下面是自己实现的switch的html

<template>
    <!--点击外层label,内层checkbox会发生改变-->
    <label class="switch">
      <input type="checkbox" v-model="value" />
      <!--内层滑动条,圆形按钮是after元素-->
      <div class="switch-checkbox"></div>
    </label>
</template>

里面的input用display:none隐藏掉,<div class="switch-checkbox"></div>才是滑块的背景,用:after伪元素来模拟里面的圆形滑块,关键是在不写js的情况下如何控制滑块的左右移动,通过checked属性就能办到,如下

input[type="checkbox"] {
      //隐藏input
      display: none;
      //利用input的checked触发滑动动画
      &:checked {
        //这里的+(相邻兄弟选择器)很重要,否则无法选择到,因为不加+就变成子元素
       +.switch-checkbox {
          background-color:@activeBgColor;
          &:after {
            transform: translateX(26px);
            background: @activeButtonColor;
            opacity: 1!important;
          }
        }
      }
    }

&:checked情况下用相邻兄弟选择器选择.switch-checkbox类里面的after伪元素,让其的transform发生改变,从而更改滑块的位置,这样就不用写一行js实现滑块的移动

Element的实现方式

先来看switch的html结构

<template>
  <div
    class="el-switch"
    :class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
    role="switch"
    :aria-checked="checked"
    :aria-disabled="switchDisabled"
    @click="switchValue"
  >
    <input
      class="el-switch__input"
      type="checkbox"
      @change="handleChange"
      ref="input"
      :id="id"
      :name="name"
      :true-value="activeValue"
      :false-value="inactiveValue"
      :disabled="switchDisabled"
      @keydown.enter="switchValue"
    >

    <!--前面的文字描述-->
    <span
      :class="['el-switch__label', 'el-switch__label--left', !checked ? 'is-active' : '']"
      v-if="inactiveIconClass || inactiveText">
      <i :class="[inactiveIconClass]" v-if="inactiveIconClass"></i>
      <span v-if="!inactiveIconClass && inactiveText" :aria-hidden="checked">{{ inactiveText }}</span>
    </span>

    <!--开关核心部分-->
    <span class="el-switch__core" ref="core" :style="{ 'width': coreWidth + 'px' }">
    </span>

    <!--后面的文字描述-->
    <span
      :class="['el-switch__label', 'el-switch__label--right', checked ? 'is-active' : '']"
      v-if="activeIconClass || activeText">
      <i :class="[activeIconClass]" v-if="activeIconClass"></i>
      <span v-if="!activeIconClass && activeText" :aria-hidden="!checked">{{ activeText }}</span>
    </span>
  </div>
</template>

最外层一个div包裹,这是为了当有文字描述时,可以点击文字也触发开关状态改变,注意这个div上绑定了点击事件@click="switchValue",这就和自己实现的方式不同了,Element的开关组件写了很多js,目的是能更好的控制一些特性实现,功能更丰富,可以猜到,switchValue这个方法就是切换开关的状态

里面先是一个input,这个input被隐藏掉,css代码如下

@include e(input) {
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
    &:focus ~ .el-switch__core {
      outline: 1px solid $--color-primary;
    }
  }

绝对定位且宽高都为0,也就是说无法点击到该input,然后是3个span并排下来,第一个和最后一个span都是文字描述,如果用户传入文字才显示,否则不显示,中间的span才是核心,很明显这个span是开关的外层椭圆背景,里面的滑块是由after伪元素实现

<span class="el-switch__core" ref="core" :style="{ 'width': coreWidth + 'px' }">
</span>

看一下el-switch__core类的内容

@include e(core) {
    margin: 0;
    display: inline-block;
    position: relative;
    width: $--switch-width;
    height: $--switch-height;
    border: 1px solid $--switch-off-color;
    outline: none;
    border-radius: $--switch-core-border-radius;
    box-sizing: border-box;
    background: $--switch-off-color;
    cursor: pointer;
    transition: border-color .3s, background-color .3s;
    vertical-align: middle;

    &:after {
      content: "";
      position: absolute;
      top: 1px;
      left: 1px;
      border-radius: $--border-radius-circle;
      transition: all .3s;
      width: $--switch-button-size;
      height: $--switch-button-size;
      background-color: $--color-white;
    }
  }

开关外层的椭圆背景是display:inline-block且相对定位,因为里面的滑块要绝对定位,:after部分就是一个绝对定位的圆形,transition: all .3s规定了滑块动画时间以及背景颜色变化的时间,但是换这个&:after只是滑块未激活状态,激活状态的css如下

@include when(checked) {
    .el-switch__core {
      border-color: $--switch-on-color;
      background-color: $--switch-on-color;
      &::after {
        left: 100%;
        margin-left: -$--switch-button-size - 1px;
      }
    }
  }

可以看到激活状态下滑块的left值变为100%,相当于移动到了右侧,而外层椭圆形背景的颜色也变化为滑块激活时的颜色

Switch的js逻辑

现在介绍下整个组件的数据传递逻辑,首先先来看用法

<el-switch
  v-model="value2"
  active-color="#13ce66"
  inactive-color="#ff4949">
</el-switch>

只需要给该组件的v-model设置一个data中的值即可,当开关开启关闭后整个value2会相应的变化,首先要知道组件的v-model用法,v-model就是@input和:value的简写,因此在组件内部要有一个value作为prop,具体看Vue官网。然后回到Switch,最外层的div绑定了click事件,代码如下

switchValue() {
   !this.switchDisabled && this.handleChange();
},

当组件不在禁用状态下时触发handleChange方法,handleChange如下

handleChange(event) {
    this.$emit('input', !this.checked ? this.activeValue : this.inactiveValue);
    this.$emit('change', !this.checked ? this.activeValue : this.inactiveValue);
    this.$nextTick(() => {
      // set input's checked property
      // in case parent refuses to change component's value
      this.$refs.input.checked = this.checked;
    });
  },

这里主要看前2句,第一句是emit了一个input,同时将开关最新的值传递出去,这就是组件v-model的用法,必须指定一个$emit('input')来改变组件上v-model的值,否则无法更新用户传入的v-model,然后第二个$emit是组件添加的change事件,用于switch 状态发生变化时的回调函数,用户可以在这里面监测开关值改变了这一事件

!this.checked ? this.activeValue : this.inactiveValue说明了如果不是激活状态,则传递出去activeValue,激活状态的值,这就是在切换状态了。那么this.checked是啥呢?来看一下

computed: {
      checked() {
        return this.value === this.activeValue;
      },
}

原来是一个计算属性,当v-model的值和激活状态的值相同时就是checked状态,反之不是,这样当this.$emit('input', !this.checked ? this.activeValue : this.inactiveValue)后checked这个计算属性也就跟着变化了,那么问题来了,handleClick后是如何控制滑块的动画效果呢?因为这里没有写任何js,

这里通过$refs.input.checked拿到了内置input的checked的值(这里通过setAttribute也可以改,),注意到最外层的div

<div
    ...
    :class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
    @click="switchValue"
  >

这里的class内有个is-checked类,它就是由checked这个计算属性控制,当checked为true时,div就添加这个is-checked类,这个类实际上啥都没有,作用是用来控制div里面滑块span的类以及after伪元素,如下

当有is-checked类时,上述css就被激活,因此改变了滑块背景的背景色和边框色,同时也改变了:after伪元素。

handleClick里面的nextTick那里不明白,有这么2句注释,这里将input的checked属性也改变了,是为了防止父组件拒绝修改value,为什么会拒绝修改呢,不太清楚

// set input's checked property
// in case parent refuses to change component's value

我试着将switch组件里面的所有input相关的内容都去掉,该组件仍然工作正常,说明input不是必须的,仔细想一下也对,上面的分析和input没有任何关系,组件内维护了activeValue和inactiveValue,通过v-model传入value,然后将value和activeValue相比来确定switch是否checked,确实不需要input

最后看一下created里面的内容

 created() {
      if (!~[this.activeValue, this.inactiveValue].indexOf(this.value)) {
        this.$emit('input', this.inactiveValue);
      }
    },

这里说明当用户传入的v-model的值既不是activeValue也不是inactiveValue时,将inactiveValue传递出去,也就是让组件置位非开启状态,这个~代表按位非运算符,如果[this.activeValue, this.inactiveValue].indexOf(this.value)为-1,则按位非后变为0,再!后变为1,为true,则进if

再说下~~和!!,前者是用来将小数向下取整的操作(~对浮点数进行了截断),后者是用来将值变为bool值