超詳細 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

相關文章
相關標籤/搜索