Element原始碼分析系列9-Switch(開關)

超級索尼發表於2018-08-29

簡介

終於遇到一個簡單的元件了,不過這個元件的實現還是和我之前的實現有所不同,下圖Element的Switch元件

Element原始碼分析系列9-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偽元素,如下

Element原始碼分析系列9-Switch(開關)
當有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值

相關文章