菜鸟读element源码六el-radio

3,297 阅读3分钟

开篇

花了快半个月时间来看radio组件,真的是发现自己基础很薄弱,很多东西都不知道,还是要多学习才是。

我发现直接把代码放出来的效果并不是很好,因为要结合着讲解来才行。因此从本篇开始,我会放出我github的地址,我模仿的代码都会放在该地址下,有兴趣看的同学可以点链接

radio的主体结构

先来看一下radio组件的主体结构吧

<template>
  <label class="el-radio">
    <span class="el-radio__input">
      <span class="el-radio__inner"></span>
        <input class="el-radio__original">
      </span>
      <!-- keydown.stop 阻止事件继续冒泡 -->
      <span class="el-radio__label" @keydown.stop>
        <slot></slot>
        <!-- 如果没有设置radio显示的值  则显示label值 -->
        <template v-if="!$slots.default">{{label}}</template>
      </span>
  </label>
</template>

一个 template的代码结构差不多就是这个样子,使用 lable标签将这个文件包裹起来,扩大了点击范围,保证了点击文字和图标都能够起到点击的效果。

我们可以看到 radio组件并没有使用原生的radio标签,这是因为 原生标签的 radio在不同的浏览器下的样式是并不相同,因此这里是将 radio隐藏起来,自己写一个 radio代替原生,统一各个浏览器样式问题。 在这里要说明的是,因为我们需要用到 原生的radio 来获取焦点并触发 change事件, 因此我们不能将原生的 radio设置为 dispaly:none或者 visibility:hiddenElement是如何做的呢?

设置opacity:0radio的透明度设置为0,并且绝对定位 使其脱离文档流,不会占据空间。这既隐藏了 radio元素又不占据 空间,并且能够获取到焦点。是一个好方法,值得参考。

radio模板的具体属性

接下来我把 radio的主体 template放出来讲解一下其中的属性

<template>
  <label
    class="el-radio"
    :class="[
      // radio大小仅在border为true时有效
      border && radioSize? 'el-radio--' + radioSize : '',
      // 是否禁用
      {'is-disabled': isDisabled},
      // 焦点是否在此处
      {'is-focus': focus},
      // 是否显示边框
      {'is-bordered': border},
      // 是否选中当前按钮
      {'is-checked': model === label}
    ]"
    role="radio"
    :aria-checked="model===label"
    :aria-disabled="isDisabled"
    :tabIndex="tabIndex"
    @keydown.space.stop.prevent="model = isDisabled ? model : label"
  >
    <span class="el-radio__input"
      :class="{
        'is-disabled': isDisabled,
        'is-checked': model === label  
      }"
    >
      <span class="el-radio__inner"></span>
        <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"
        >
      </span>

      <!-- keydown.stop 阻止事件继续冒泡 -->
      <span class="el-radio__label" @keydown.stop>
        <slot></slot>
        <!-- 如果没有设置radio显示的值  则显示label值 -->
        <template v-if="!$slots.default">{{label}}</template>
      </span>
  </label>
</template>
role = 'radio'
:aria-checked="model===label"
:aria-disabled="isDisabled"
//这三行是为了给不方便人士使用时提供功能的。比如当他们使用屏幕阅读器的时候, role的作用是告诉阅读这是一个 radio, aria-checked是描述这个 radio是否被选择, aria-disabled是告诉阅读器这个按钮不可读。
tabIndex="tabIndex"  // 设置是否可以通过键盘上的 tab键 进行选择,  -1 代表不可选, 0 代表可选

label标签上的 tabIndex是通过计算得到的,

isGroup() {
      let parent = this.$parent
      while (parent) {
        if (parent.$options.componentName !== 'ElTestRadioGroup') {
          parent = parent.$parent
        } else {
          // eslint-disable-next-line
          this._radioGroup = parent
          return true
        }
      }
      return false
    },
    // 是否禁用
    isDisabled() {
      return this.isGroup
        ? this._radioGroup.disabled || this.disabled || (this.elTestForm || {}).disabled
        : this.disabled || (this.elTestForm || {}).disabled
    },
tabIndex() {
  return (this.isDisabled || (this.isGroup && this.model !== this.label))  ? -1 : 0
}

首先这里通过遍历 查询当前radio是否来被包裹于一个 radio-group组件当中,根据是否包裹于 radio-groupisGroup的值设置为 truefalse。 再根据 isGroup来判断 isDisabled

isGrouptrue时, isDisabled的值首先取决于 radioGroup是否禁用,然后是radio是否禁用, 最后有可能 radio是位于一个 form表单当中,取决于 form的禁用状态(看来form会是一个 大难点哦)。 当为 false时,取决于 isGroup

同样的 tabIndex值取决于 禁用状态 , 并且当 radio位于 radioGroup时, 并且选中的是当前 radio, 则保证使用 tab键操作的时候,不会再次选择,优化了体验。

在这几块的计算判断当中,值得注意的是它并没有使用if else判断,而是使用了 与或的短路原则,值得学习一下。

&& 的判断是同真为真,一假为假,则运算如果左边的表达式值为 false,那么就不会再执行右边的表达式了,如果左表达式为 true,就会继续执行右表达式 || 的判断是一真为真,同假为假,则运算如果左表达式值为 true,那么就不用执行右边的表达式了,如果左表达式为 false,就会继续执行右表达式;

template标签上还值得学习的一点是vue的事件修饰符。事件修饰符。vue为事件v-on添加了以下修饰符

疑问一 passive究竟怎么使用的诶?

vue还支持按键修饰符、鼠标键值修饰符、键值修饰符。详细的解释可以看这篇文章,写的很详细。按键修饰符

介绍了按键修饰符,那么这句代码也就 不难理解了

@keydown.space.stop.prevent="model = isDisabled ? model : label"

tab选中当前 radio 在键盘上敲击空格键的时候(space即空格键 ),阻止了原生事件发生(发生了什么原生事件?这个不太知道),并执行代码

model = isDisabled ? model : label   // 键盘选中radio

不知道你们看了 radio组件有没有一个疑惑,就是没有一个 click事件,却在点击的时候触发了 model值的改变? 因为这块有重写v-model, 我打印了 modelsethandleChange

  model: {
      get() {
      },
      set(val) {
        console.warn(val)
       }
    },
    
    handleChange() {
      console.log('change')
    }

在点击事件触发后执行顺序 为 set->handleChange

在这个地方我们不得不提及一下 v-model的实现原理

v-model的本质是一个语法糖(几乎说烂的词),其本质是 v-bind v-on的组合,以下两种情况是相等的

<input v-model="test"></input>

<input v-bind:value="test" v-on:input = "test = $event.target.value"

我们来看一下为什么这两种情况是相等的。

从源码的角度,我们使用v-model的时候其实是触发了这个函数

function genRadioModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  let valueBinding = getBindingAttr(el, 'value') || 'null'
  valueBinding = number ? `_n(${valueBinding})` : valueBinding
  addProp(el, 'checked', `_q(${value},${valueBinding})`)
  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
}

你可能会发现 为什么添加的是 checked属性 和change事件

其实v-model在不同的 HTML标签上会监控不同属性,抛出不同事件

  • text和textarea元素使用 value属性和 input事件
  • checkboxradio元素使用 checked属性和 change事件
  • selectvalue作为prop并将change作为事件
  • 在自定义组件上 v-model使用 value属性 和 input事件 我们可以看到源码上函数genRadioModel确实是给 input组件添加了 checked属性 和change事件。 源码上对于input标签的不同类型也是做不同的处理(我这里放出部分代码),详细的过程有兴趣的同学可以去看VUE的源码 或者 去看 vue.js技术揭秘 好书值得一看!!!强推
  if (el.component) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `<${el.tag} v-model="${value}">: ` +
      `v-model is not supported on this element type. ` +
      'If you are working with contenteditable, it\'s recommended to ' +
      'wrap a library dedicated for that purpose inside a custom component.',
      el.rawAttrsMap['v-model']
    )
  }

所以原生代码时 <input type="radio" v-model="value"> 经过处理后 按照 源码的流程,将radio其实处理为 <input v-bind:checked="value" v-on:change="value = $event.target.checked">

因此当我们引入 radio组件并使用的时候,我们的代码是这样

<el-radio v-model="radio"></el-radio>
等价于
<el-radio  v-bind:value="radio" v-on:input="radio = #event.tagret.value"></el-radio>

在代码中的原生 input标签

<input 
...
          v-model="model"
          type="radio"
.... 
        >

则转化为

<input  v-bind: checked="model" v-on: change="model=$event.target.checked">

当 原生 radio被点击时, model的值发生改变,触发 set

model: {
      get() {
        // 如果是以el-radio-group包裹 则取group的value值
        return this.isGroup ? this._radioGroup.value : this.value
      },

      set(val) {
        if (this.isGroup) {
          this.dispatch('ElTestRadioGroup', 'input', [val])
        } else {
          this.$emit('input', val)
        }
        this.$refs.radio && (this.$refs.radio.checked = this.model === this.label)
      }
    },

然后调用 this.$emit('input')将值传递到 我们自定义的radio组件上,进而改变我们绑定的 radio值。 利用了 on/emit监听传递事件。

此时如果是radio-group时,会触发 dispatch事件,在非group时,触发 input事件。

 <input 
   ...
          @focus="focus = true"
          @blur="focus=false"
          @change="handleChange"
...
        >

最后就是在鼠标焦点聚焦 radio以及移开时 触发 focus以及 blur事件, 当model的值发生改变时会触发 handleChange事件

    handleChange() {
      this.$nextTick(() => {
        this.$emit('change', this.model)
        // 根据是否是group调用 分发dispatch事件
        this.isGroup && this.dispatch('ElTestRadioGroup', 'handleChange', this.model)
      })
    }

this.$emit('change', this.model)则是如果组件有自定义的 change事件时,将会触发自定义的change事件。

疑问二

如何对vue源码中打断点呢? 在运行时,vue执行的是node_modules/vue/dist/vue.runtime.ems.js, 但是dis`是打包后的文件,我如何对 src/...里面 分开的单个文件打断点并查看?

比如说这样一个文件,model.js,用来处理 v-model指令的,我怎么打断点然后在运行时查看呢?求教

后续

其实 radio其实还引用了 mixins属性(混入), 因为在单个的 radio中并未触发到 混入文件 emitter.js中的函数,因此我会放在下一篇 radio-group时来根据事件触发的顺序讲一下 混入函数的触发过程。

总结

感觉可以开始阅读vue源码了,很多东西还是要和源码结合才能看懂诶,一个radio看了快半个月,大部分都花在源码上了。望诸君一起加油。有问题希望大家指出来!