超详细 ElementUI 源码分析 —— Radio

Radio 单选按钮

前置知识

本文继续带你看表单组件 radio,若是你没有读过另外一篇文章 Input,我建议你先看完那个再来,由于不少东西在那里面分析了。html

首先让咱们来了解一下 radio 在表单中的做用:前端

  • radio 是单选按钮,当咱们点击的时候会有选中和不选中状态
  • 在单选按钮组里用于选择多个相互排斥的按钮

原生的 radio 想必你们都很熟悉,平时开发中也会常常用到,先看一下它经常使用的两个属性vue

  • name单选按钮的名称
  • value单选按钮须要传给服务器的值

这里重点关注一下value,它在前端页面上并不会起到什么做用,甚至不会显示,可是最主要的就是能够经过它将单选按钮选择的值传递给服务器,好让后台程序知道用户选择了什么。为何要讲这个,别急,对后面的理解确定有帮助。node

既然 ElementUI 是基于 Vue 开发的,那么在 Vue 中是如何使用 radio 的呢?ios

移步官网查看表单输入绑定git

从官网中咱们能够看到实现 radio 的双向绑定也是使用了v-model语法糖,它会根据控件类型自动选取正确的方法来更新元素。那么在 radio 组件中就等效于给value属性和input事件同时绑定一个响应式数据。当选中单选按钮时v-model绑定的值一般就是value的值github

<!-- 当选中时,`picked` 为字符串 "a" -->
<input type="radio" v-model="picked" value="a">
复制代码

基本结构

<template>
  <label class="el-radio">
    <span class="el-radio__input">
      <span class="el-radio__inner"></span>
      <input type="radio" />
    </span>
    <span class="el-radio__label">
      <slot></slot>
      <template v-if="!$slots.default">{{ label }}</template>
    </span>
  </label>
</template>
复制代码

从这个结构中咱们能够看出整个 radio 组件包裹在 label 标签中,首先简单了解一下 label 标签,它是为 input 元素定义标记的,label 元素不会向用户呈现任何特殊效果,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上,也就是说当咱们点击 label 时,其实就是点击了内部的 input 标签。这样作为用户点击按钮提供了便捷,只要在 label 范围内点击都能触发按钮的事件。编程

里层有两个span标签,第一个表示的是前面的小圆圈,因为各浏览器对于标签的默认样式不统一,咱们又须要保证咱们的组件在任何地方运行都能保持一致的效果,一般会把原生 input 样式隐藏起来,经过一个span或者div来模拟,源码的具体样式后期再分析。第二个span表示的是按钮后面的显示文字,默认传递的内容会在插槽中渲染,若是用户没有直接传递内容,那么就须要把用户传递的 label 值显示出来,这就是后面的template作的事。json

radio 属性

接下来重点看一下 input 属性浏览器

<input ref="radio" class="el-radio__original" :value="label" type="radio" aria-hidden="true" v-model="model" @focus="focus = true" @blur="focus = false" @change="handleChange" :name="name" :disabled="isDisabled" tabindex="-1" />
复制代码

这里面会涉及 radio 组件的属性和方法,因此我打算放在一块儿来分析。

有一些在个人另外一篇文章 Input里面分析过,这里就再也不赘述,建议传送过去看一下。

  • 经过el-radio__original类将 input 隐藏同时还能触发事件
  • :value是把父组件传递过来的 label 给原生value属性
  • :name绑定原生name

这里重点要说的是v-model="model",这个怎么理解呢?不急,慢慢解释。

当咱们须要在多个单选按钮中里面来控制同一个值时,要使用v-model双向绑定一个值,这个值是什么?看一个例子

<template>
  <el-radio v-model="gender" label="0"></el-radio>
  <el-radio v-model="gender" label="1"></el-radio>
</template>
复制代码
data() {
  return {
    gender: '0'
  }
}
复制代码

使用组件发现v-model绑定的是父组件的gender值,可是子组件自己不可以直接使用v-model='value'来双向绑定数据,由于这个value来自于父组件,根据 Vue 的单项数据流,子组件通常状况下是不能直接更改父组件传递过来的数据的,因此须要定义一个本身的model用来绑定 radio 组件的数据,可是这个model也不是经过data来写死的,由于它是取决于外界传进来的value值,同时还要修改这个值。

从源码中能够看到,model是一个计算属性,当它被当成一个双向数据绑定的值时,就不能是一个函数,而是一个对象,提供getset方法。

model: {
  get () {
    // 若是在一个单选按钮组里就是按钮组的值
    return this.isGroup ? this._radioGroup.value : this.value
  },
  set (val) {
    // 若是是 radio 包裹在按钮组里,那么 model 的改变就须要触发父组件的 input 事件
    if (this.isGroup) {
      this.dispatch('ElRadioGroup', 'input', [val])
    } else {
      this.$emit('input', val)
    }
    // 若是当前的 model 等于组件的 label 就表示当前这个按钮被选中了
    this.$refs.radio &&
      (this.$refs.radio.checked = this.model === this.label)
  }
}
复制代码

有关dispatch方法参考另外一篇文章 Input,里面有很是详细的分析emitter.js文件的详细解释也以及上传到个人 Github 上了,欢迎 star!

当咱们须要获取model值时会调用它的get方法,get方法里面会判断 radio 是否是在一个单选按钮组里(具体见下文),若是在,那么 radio 自己是没有value的,value是经过radio-groupv-model传递过来的,如今咱们来看一下isGroup这个计算属性:

// 判断 radio 标签是否在按钮组里
isGroup () {
  let parent = this.$parent
  while (parent) {
    if (parent.$options.componentName !== 'ElRadioGroup') {
      parent = parent.$parent
    } else {
      // 将按钮组添加到当前组件实例的 _radioGroup 属性上
      // 并结束循环
      this._radioGroup = parent
      return true
    }
  }
  return false
}
复制代码

在这个属性里经过循环遍历父组件,直到找到radio-group组件把它赋值给_radioGroup,而后咱们就可以在 radio组件中使用radio-group组件实例中的数据和方法了,这里为何不能直接用$parent呢?是由于有考虑到组件嵌套,若是radio组件的直接父组件不是radio-group,那$parent指向的就不是咱们须要的组件,因此这里一直遍历$parent$parent就是为了将parent指向正确的组件。

固然这里咱们还可使用「依赖注入」的方法去实现深层次的父子通讯,在radio-group中定义须要注入的属性,将radio-group组件的实例传过去:

provide () {
  return {
    radioGroup: this
  }
}
复制代码

radio组件使用inject来接收,默认是一个空字符串:

inject: {
  radioGroup: {
    default: ''
  }
}
复制代码

至于methods里面的方法handleChange是用来处理 radio 的 change 事件的,可是目前还不明白为何要使用nextTick(),但愿大佬能分享一下。

handleChange () {
  // 这里不太清楚为何使用 nextTick() 
  this.$nextTick(() => {
    this.$emit('change', this.model)
    // 若是存在按钮组
    this.isGroup &&
      this.dispatch('ElRadioGroup', 'handleChange', this.model)
  })
}
复制代码

RadioGroup 单选按钮组

ElementUI 提供了单选按钮组来包裹一组互斥的按钮,使得咱们在使用的时候只须要将v-model双向绑定在radio-group上,而在radio里面只须要传入label便可。来看一下RadioGroup组件的封装:

基本结构

<template>
  <component :is="_elTag" class="el-radio-group" role="radiogroup" @keydown="handleKeydown" >
    <slot></slot>
  </component>
</template>
复制代码

能够看出来结构仍是很简单的,就是一个component包裹了一个插槽,使用:is来决定须要将component渲染成哪一个组件,_elTag是一个计算属性

_elTag() {
  return (this.$vnode.data || {}).tag || 'div';
}
复制代码

返回的是当前组件的虚拟 DOM 节点的标签,默认是div

接着往script里面看,一开始就定义了一个对象,可是这个对象又不是普通的对象,它是被冻结起来的,这里就要详细说一下Object.freeze()的做用了。

该方法用于冻结一个对象,被冻结的对象不能够修改,也就是对象身上不能添加或者删除属性,也不能修改「已有属性的可枚举性、可配置性、可写性以及原有的值」。另外它的原型对象也不容许修改,返回的是这个对象自己而不是副本。若是强行修改对象,通常状况下是静默失败的,也就是不会报错,可是在「严格模式」下会抛出TypeError异常。

值得一提的是它是「浅冻结」,对于被冻结的对象中若是某个属性是一个「引用类型」的值,那么引用类型的值只要地址不发生变化它的属性值是能够更改的,要想实现「深冻结」就须要使用递归循环对象,具体实现参考 MDN Object.freeze()

// 冻结对象
const keyCode = Object.freeze({
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40
});
复制代码

源码是将键盘上的「上下左右」按键的键盘码做为一个对象冻结起来了,这样可以防止后续代码不当心修改了它的值。那么为何要使用键盘码呢?

在实际使用过程当中,能够发现使用方向键能够切换选中的按钮,这也是为了使用过程当中尽可能减小手离开键盘吧

// 左右上下按键 能够在 radio 组内切换不一样选项
handleKeydown(e) {
  // 事件触发的元素
  const target = e.target;
  // 若是当前的不是 input 元素,那就是 label 标签
  const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
  const radios = this.$el.querySelectorAll(className);
  const length = radios.length;
  // 拿到事件触发元素在全部 radio 里的索引
  const index = [].indexOf.call(radios, target);
  const roleRadios = this.$el.querySelectorAll('[role=radio]');
  switch (e.keyCode) {
    case keyCode.LEFT:
    case keyCode.UP:
      e.stopPropagation();
      e.preventDefault();
      // 索引为 0 表示当前按钮的第一个触发了键盘事件
      if (index === 0) {
        // 选中最后一个
        roleRadios[length - 1].click();
        roleRadios[length - 1].focus();
      } else {
        // 往前选择
        roleRadios[index - 1].click();
        roleRadios[index - 1].focus();
      }
      break;
    case keyCode.RIGHT:
    case keyCode.DOWN:
      // 若是当前是最后一个,那么接下来就选中第一个
      if (index === (length - 1)) {
        e.stopPropagation();
        e.preventDefault();
        roleRadios[0].click();
        roleRadios[0].focus();
      } else {
        // 日后选择
        roleRadios[index + 1].click();
        roleRadios[index + 1].focus();
      }
      break;
    default:
      break;
  }
}
复制代码

分析一下具体实现的步骤:

  • 在事件被触发的时候判断按下键盘时触发该事件的元素
  • 拿到触发事件的元素在整个按钮组里的位置
  • 判断按下的是哪个按键,并触发 label 标签的clickfocus事件

最后还有一个监听属性value

// 监听 value 若是发生变化就 form-item 组件触发 change 事件
watch: {
  value(value) {
    this.dispatch('ElFormItem', 'el.form.change', [this.value]);
  }
}
复制代码

由于咱们的组件最终确定都是会放在 form 表单里面的,监听value是为了当它的值发生变化时及时通知 form 表单更新数据。

总结与梳理

最后来总结一下 radio 组件的封装:

  • radio 组件的功能是在已有选项中选择数据
  • 使用了 label 标签包裹
  • 提供了v-model进行数据双向绑定
  • 提供了radio-group包裹radio使得在一组里面只能单一选择
  • 提供了方向键切换选中的按钮

虽然提供的功能很简单,经过阅读和分析,咱们的编程能力必定会有所提升的,再本身动手封装一个 radio 组件你会更加了解一个组件的封装须要考虑哪些问题。

OK,下一篇再见。

传送门

【2020.3.15】超详细 ElementUI 源码分析 —— Input

【2020.3.16】超详细 ElementUI 源码分析 —— Layout

相关文章
相关标签/搜索