Element源码分析系列6-Checkbox(复选框)

栏目: 编程工具 · 发布时间: 5年前

内容简介:复选框的逻辑比单选框更为复杂,代码量也更多,这里只介绍其与单选框不同的逻辑,其余的分析参考单选框是不是看的一脸懵逼,最好是打开官网,对照checkbox用法一项项来分析其原理同单选框类似,复选框的示意图如下,无非就是左右2部分组成,外层套一个label,并隐藏原生的

复选框的逻辑比单选框更为复杂,代码量也更多,这里只介绍其与单选框不同的逻辑,其余的分析参考单选框

Element源码分析系列6-Checkbox(复选框)
先上代码,官网代码 点此
<template>
  <label
    class="el-checkbox"
    :class="[
      border && checkboxSize ? 'el-checkbox--' + checkboxSize : '',
      { 'is-disabled': isDisabled },
      { 'is-bordered': border },
      { 'is-checked': isChecked }
    ]"
    role="checkbox"
    :aria-checked="indeterminate ? 'mixed': isChecked"
    :aria-disabled="isDisabled"
    :id="id"
  >
    <span class="el-checkbox__input"
      :class="{
        'is-disabled': isDisabled,
        'is-checked': isChecked,
        'is-indeterminate': indeterminate,
        'is-focus': focus
      }"
       aria-checked="mixed"
    >
      <span class="el-checkbox__inner"></span>
      <input
        v-if="trueLabel || falseLabel"
        class="el-checkbox__original"
        type="checkbox"
        aria-hidden="true"
        :name="name"
        :disabled="isDisabled"
        :true-value="trueLabel"
        :false-value="falseLabel"
        v-model="model"
        @change="handleChange"
        @focus="focus = true"
        @blur="focus = false">
      <input
        v-else
        class="el-checkbox__original"
        type="checkbox"
        aria-hidden="true"
        :disabled="isDisabled"
        :value="label"
        :name="name"
        v-model="model"
        @change="handleChange"
        @focus="focus = true"
        @blur="focus = false">
    </span>
    <span class="el-checkbox__label" v-if="$slots.default || label">
      <slot></slot>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';
  export default {
    name: 'ElCheckbox',
    mixins: [Emitter],
    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },
    componentName: 'ElCheckbox',
    data() {
      return {
        selfModel: false,
        focus: false,
        isLimitExceeded: false
      };
    },
    computed: {
      model: {
        get() {
          return this.isGroup
            ? this.store : this.value !== undefined
              ? this.value : this.selfModel;
        },
        set(val) {
          if (this.isGroup) {
            this.isLimitExceeded = false;
            (this._checkboxGroup.min !== undefined &&
              val.length < this._checkboxGroup.min &&
              (this.isLimitExceeded = true));
            (this._checkboxGroup.max !== undefined &&
              val.length > this._checkboxGroup.max &&
              (this.isLimitExceeded = true));
            this.isLimitExceeded === false &&
            this.dispatch('ElCheckboxGroup', 'input', [val]);
          } else {
            this.$emit('input', val);
            this.selfModel = val;
          }
        }
      },
      isChecked() {
        if ({}.toString.call(this.model) === '[object Boolean]') {
          return this.model;
        } else if (Array.isArray(this.model)) {
          return this.model.indexOf(this.label) > -1;
        } else if (this.model !== null && this.model !== undefined) {
          return this.model === this.trueLabel;
        }
      },
      isGroup() {
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName !== 'ElCheckboxGroup') {
            parent = parent.$parent;
          } else {
            this._checkboxGroup = parent;
            return true;
          }
        }
        return false;
      },
      store() {
        return this._checkboxGroup ? this._checkboxGroup.value : this.value;
      },
      isDisabled() {
        return this.isGroup
          ? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled
          : this.disabled || (this.elForm || {}).disabled;
      },
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      checkboxSize() {
        const temCheckboxSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        return this.isGroup
          ? this._checkboxGroup.checkboxGroupSize || temCheckboxSize
          : temCheckboxSize;
      }
    },
    props: {
      value: {},
      label: {},
      indeterminate: Boolean,
      disabled: Boolean,
      checked: Boolean,
      name: String,
      trueLabel: [String, Number],
      falseLabel: [String, Number],
      id: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
      controls: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
      border: Boolean,
      size: String
    },
    methods: {
      addToStore() {
        if (
          Array.isArray(this.model) &&
          this.model.indexOf(this.label) === -1
        ) {
          this.model.push(this.label);
        } else {
          this.model = this.trueLabel || true;
        }
      },
      handleChange(ev) {
        if (this.isLimitExceeded) return;
        let value;
        if (ev.target.checked) {
          value = this.trueLabel === undefined ? true : this.trueLabel;
        } else {
          value = this.falseLabel === undefined ? false : this.falseLabel;
        }
        this.$emit('change', value, ev);
        this.$nextTick(() => {
          if (this.isGroup) {
            this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
          }
        });
      }
    },
    created() {
      this.checked && this.addToStore();
    },
    mounted() { // 为indeterminate元素 添加aria-controls 属性
      if (this.indeterminate) {
        this.$el.setAttribute('aria-controls', this.controls);
      }
    },
    watch: {
      value(value) {
        this.dispatch('ElFormItem', 'el.form.change', value);
      }
    }
  };
</script>
复制代码

是不是看的一脸懵逼,最好是打开官网,对照checkbox用法一项项来分析其原理

复选框整体html结构

同单选框类似,复选框的示意图如下,无非就是左右2部分组成,外层套一个label,并隐藏原生的 <input type='checkbox'>

Element源码分析系列6-Checkbox(复选框)

简化的html结构如下所示

<label ...>
    <span class='el-checkbox__input'>
        <span class='el-checkbox__inner'></span>
        <input type='checkbox' .../>
    </span>
    <span class='el-checkbox__label'>
        <slot></slot>
        <template v-if="!$slots.default">{{label}}</template>
    </span>
</label>
复制代码

这里具体参考上一篇单选按钮的文章,重点说下上图的蓝色方框内的勾是怎么实现的,也就是选中状态,开始我以为是一个类似Icon的东西,然而并不是,查看css代码如下

&::after {
      box-sizing: content-box;
      content: "";
      border: 1px solid $--checkbox-checked-icon-color;
      border-left: 0;
      border-top: 0;
      height: 7px;
      left: 4px;
      position: absolute;
      top: 1px;
      transform: rotate(45deg) scaleY(0);
      width: 3px;
      transition: transform .15s ease-in .05s;
      transform-origin: center;
}
复制代码

很明显,这是 el-checkbox__inner 类的after伪元素,里面是一个只有右下border的长方形经过旋转45度后的图形,也就是一个勾的形状,所以这个勾只是纯粹的css实现而已,好处是简化了html结构,并且还用了 transition 来添加点击后勾变大的动画效果,这里是通过过渡 transformscaleY 的值来实现,未选中时 scaleY 为0,选中时为1,就实现了勾放大的效果

因此要善用伪元素,会简化很多不必要的代码

Vue中的复选框原理

首先来看Vue中的复选框是怎么实现的,了解这个有助于理解Element的实现,官网介绍如下

Element源码分析系列6-Checkbox(复选框)

上图中单个复选框使用bool值,多个复选框使用数组即可,这里其实Vue在幕后做了许多工作,找到Vue中相关源码如下

function genCheckboxModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  const valueBinding = getBindingAttr(el, 'value') || 'null'
  const trueValueBinding = getBindingAttr(el, 'true-value') || 'true'
  const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
  addProp(el, 'checked',
    `Array.isArray(${value})` +
    `?_i(${value},${valueBinding})>-1` + (
      trueValueBinding === 'true'
        ? `:(${value})`
        : `:_q(${value},${trueValueBinding})`
    )
  )
  addHandler(el, 'change',
    `var $$a=${value},` +
        '$$el=$event.target,' +
        `$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
    'if(Array.isArray($$a)){' +
      `var $$v=${number ? '_n(' + valueBinding + ')' : valueBinding},` +
          '$$i=_i($$a,$$v);' +
      `if($$el.checked){$$i<0&&(${genAssignmentCode(value, '$$a.concat([$$v])')})}` +
      `else{$$i>-1&&(${genAssignmentCode(value, '$$a.slice(0,$$i).concat($$a.slice($$i+1))')})}` +
    `}else{${genAssignmentCode(value, '$$c')}}`,
    null, true
  )
}
复制代码

这就是处理checkbox的v-model的代码,我们只需要知道这段代码大概在做啥就行,细节不用太清楚,代码中首先获取 v:bind 绑定的value的值,也就是下面示例代码中的 Jack (注意代码中其实处理了不是v:bind的情况,具体看源码),然后 genCheckboxModel 这个函数的参数中的 value 就是v-model的值,也就是下面的 checkedNames ,接下来 addProp 方法的逻辑:如果 checkedNames 是数组,则通过indexOf查询 Jack 是否在 checkedNames 中,如果在则给input添加checked属性代表被选中,其次如果 checkedNames 不是数组,则直接比较2者是否相等来决定是否给input添加checked属性

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">

由上面的分析可见, 复选框的选中的checked属性是Vue幕后添加的,通过值的比较来决定是否添加该属性

然后来看 addHandler 方法,这个方法给复选框添加了change事件,原生复选框点击后它的checked属性会改变(true或false),但是Vue中的 checkedNames 的值会跟着变化,这里就是 addHandler 所做的工作了,该方法里面总体逻辑就是首先判断 checkedNames 是否是数组,如果是且该复选框被选中,则将该复选框的值加入 checkedNames 数组中,如果该复选框没有被选中,则从数组中去掉它(注意这里没有用splice,而是2个slice后concat合并成一个数组,splice会改变原始数组,这样就不会)

所以onchange这个事件也是Vue幕后处理的,因此 checkedNames 数组就能够随着我们点击不同的复选框而同步变化

checkbox源码分析

接下来我们按官网罗列的功能依次分析

禁用功能

禁用功能最简单,使用起来如下代码,只需添加 disabled 属性即可

<el-checkbox v-model="checked1" disabled>备选项1</el-checkbox>
复制代码

源码里对应的 :disabled 属性

<input
    v-else
    class="el-checkbox__original"
    type="checkbox"
    aria-hidden="true"
    :disabled="isDisabled"
    :value="label"
    :name="name"
    v-model="model"
    @change="handleChange"
    @focus="focus = true"
    @blur="focus = false"
>
复制代码

,这里 isDisabled 是个计算属性,因为要考虑到复选框组组件的存在,复选框组组件 <el-checkbox-group> 也有 disabled 属性,且复选框组组件是复选框组件的父级组件

isDisabled() {
        return this.isGroup
          ? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled
          : this.disabled || (this.elForm || {}).disabled;
      },
复制代码

这里首先判断自己是不是被包含在复选框组组件内,如果是的话那么禁用属性就是父级的复选框组组件的禁用属性,否则就是自己的属性,关于如何判断是否被包含在复选框组组件内,前面系列文章已经介绍过了

多选框组组件

这里翻看Vue官网,示例代码说明 仅仅需要把多个复选框的input的v-model设置为同一个数组就能达到复选框组的目的

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames">
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
  <label for="mike">Mike</label>
复制代码

但是查看Element的代码,还单独抽象出了一个 <el-checkbox-group> 组件,这样做的好处在于不用给每个复选框组件设置v-model,只需给 <el-checkbox-group> 设置v-model即可,且这个抽象出的组件还能添加其他很多自定义的属性,相当于添加一个父组件统一控制所有子复选框的某些行为

<el-checkbox-group> 的代码很简单,html部分如下

<template>
  <div class="el-checkbox-group" role="group" aria-label="checkbox-group">
    <slot></slot>
  </div>
</template>
复制代码

就是一个div里面放置了一个插槽,插槽的内容就是用户放进去的 <el-checkbox> 组件,多选框框组组件的props如下

props: {
      value: {},
      disabled: Boolean,
      min: Number,
      max: Number,
      size: String,
      fill: String,
      textColor: String
    },
复制代码

其中 value 是组件上v-model的用法,具体参考官网和前面文章的说明,这里的 min,max 属性控制了复选框框组最多和最少能选择的复选框数量,这是怎么实现的呢?

首先查看源码注意到,这里的逻辑并没有放在 <el-checkbox-group> 里实现,而是放在 <el-checkbox> 里实现,因为你实际点击的是 <el-checkbox> 的input,所以需要在复选框组件内实现逻辑,相关代码如下

model: {
        get() {
          return this.isGroup
            ? this.store : this.value !== undefined
              ? this.value : this.selfModel;
        },
        set(val) {
          if (this.isGroup) {
            this.isLimitExceeded = false;
            (this._checkboxGroup.min !== undefined &&
              val.length < this._checkboxGroup.min &&
              (this.isLimitExceeded = true));
            (this._checkboxGroup.max !== undefined &&
              val.length > this._checkboxGroup.max &&
              (this.isLimitExceeded = true));
            this.isLimitExceeded === false &&
            this.dispatch('ElCheckboxGroup', 'input', [val]);
          } else {
            this.$emit('input', val);
            this.selfModel = val;
          }
        }
      },

复制代码

这个model是计算属性,在input里面的 v-model="model" 处使用,代表复选框组件v-model的值,计算属性的get,set用法参考官网,先看get,首先判断是否被包含在复选框组组件内,如果是的话,model的值就等于 this.store ,这个store也是个计算属性,如下

store() {
        return this._checkboxGroup ? this._checkboxGroup.value : this.value;
      },
复制代码

它也要判断是否被包含在复选框组组件内,如果是则返回复选框组组件的value,这个value就是下面示例代码中的checkList

<el-checkbox-group v-model="checkList">
复制代码

因此这里就把用户传递进去的checkList这个数组给传递到了子 <el-checkbox> 内,而 this._checkboxGroup 是在 isGroup 这个计算属性中赋值的,它就是自己外层的 <el-checkbox-group> 组件

再来看set方法,set是给model赋值时触发的方法,会在用户点击复选框时触发复选框的onchange事件,在这个事件里面赋值,从而触发set方法,set方法里面用一个 isLimitExceeded 变量来判断是否超出max和min的限制,如果min属性存在,且val数组的长度小于min,则说明已经越界,此时设置 isLimitExceeded 为true,max同理,val的值是在Vue源码里处理的,这里不用深究,后面当 isLimitExceeded 为false时也就是未越界时才用dispatch通知父组件自己更新后的val的值

一个疑惑是在 <el-checkbox> 内的input上绑定了 @change="handleChange" ,代码如下

handleChange(ev) {
        if (this.isLimitExceeded) return;
        let value;
        if (ev.target.checked) {
          value = this.trueLabel === undefined ? true : this.trueLabel;
        } else {
          value = this.falseLabel === undefined ? false : this.falseLabel;
        }
        this.$emit('change', value, ev);
        this.$nextTick(() => {
          if (this.isGroup) {
            this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
          }
        });
      }
复制代码

上面的handleChange同样向父checkGroup组件dispatch了value,那么这个dispatch和上面的dispatch的区别在哪里呢?仔细分析后发现这里的dispatch仅仅是通知 <el-checkbox-group> 自己的值变化了,在 <el-checkbox-group> 上可以用@change来获取变化后的值(用户可以拿到该值进行进一步处理),而前面的dispatch则更新了 <el-checkbox-group> 的v-model属性的值,这2个dispatch的作用是不同的,请仔细理解

然后handleChange里 this.$emit('change', value, ev) 表示将value和ev原生事件对象传递给 <el-checkbox> 的onchange事件,因为用户可能需要这个接口来获取更新后的数据

最后再来看看当选中复选框时,css样式变化的逻辑

<span class="el-checkbox__input"
      :class="{
        'is-disabled': isDisabled,
        'is-checked': isChecked,
        'is-indeterminate': indeterminate,
        'is-focus': focus
      }"
       aria-checked="mixed"
    >
复制代码

这个span代表模拟的复选框按钮,其中 is-checked 类代表选中时的样式类,这个类由 isChecked 控制,这是个计算属性,代码如下

isChecked() {
        if ({}.toString.call(this.model) === '[object Boolean]') {
          return this.model;
        } else if (Array.isArray(this.model)) {
          return this.model.indexOf(this.label) > -1;
        } else if (this.model !== null && this.model !== undefined) {
          return this.model === this.trueLabel;
        }
      },
复制代码

第一步判断 this.model 是不是bool类型,注意这里的判断方法, Object.prototype.toString.call 来判断才是最可靠的,当model是bool时说明这个值就控制这个复选框他自己,如果这个model是数组,则判断label在不在该数组中,如果在则表示选中了该复选框,从而 isChecked 为true,label是用户定义在复选框上的属性,代表该复选框的值,具体看官网

主要内容差不多这么多,其实还有很多细节没写完,具体可以参考源码啦


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

失控的未来

失控的未来

[美]约翰·C·黑文斯 / 仝琳 / 中信出版集团 / 2017-4-1 / 59.00元

【编辑推荐】 20年前,尼古拉•尼葛洛庞帝的《数字化生存》描绘了数字科技给人们的工作、生活、教育和娱乐带来的冲击和各种值得思考的问题。数字化生存是一种社会生存状态,即以数字化形式显现的存在状态。20年后,本书以一种畅想的形式,展望了未来智能机器人与人类工作、生活紧密相联的场景。作者尤其对智能机器人与人类的关系,通过假设的场景进行了大胆有趣的描述,提出了人工智能的未来可能会面临的一些问题。黑文......一起来看看 《失控的未来》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器