element 源碼學習二 —— 簡單組件學習

上一篇博客中學習了項目的結構,這篇博客來學幾個簡單的組件的實現。

在上一篇博客中我們提到了組件的源碼都是存放在 packages 目錄下的,所以我們從中挑一些組件來學習。先從簡單的入手,來學習 button、radio、checkbox和InputNumber這四個組件的源碼~

Button

找到 packages/button/ 目錄下,先看看 index.js 做了什么?

import ElButton from './src/button';

// 注冊全局組件
ElButton.install = function(Vue) {
  Vue.component(ElButton.name, ElButton);
};

export default ElButton;

其實就是獲取組件,然后全局注冊組件。很好理解,我們自定義組件也經常這么干。看看組件內容吧~

<!-- packages/button/src/button.vue -->
<template>
  <button
    class="el-button"
    @click="handleClick"
    :disabled="disabled || loading"
    :autofocus="autofocus"
    :type="nativeType"
    :class="[
      type ? 'el-button--' + type : '',
      buttonSize ? 'el-button--' + buttonSize : '',
      {
        'is-disabled': disabled,
        'is-loading': loading,
        'is-plain': plain,
        'is-round': round
      }
    ]"
  >
    <i class="el-icon-loading" v-if="loading"></i>
    <i :class="icon" v-if="icon && !loading"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>
<script>
  export default {
    name: 'ElButton',
    // 獲取父級組件 provide 傳遞下來的數據。
    inject: {
      elFormItem: {
        default: ''
      }
    },

    // 屬性 http://element-cn.eleme.io/#/zh-CN/component/button
    props: {
      // 類型 primary / success / warning / danger / info / text
      type: {
        type: String,
        default: 'default'
      },
      // 尺寸 medium / small / mini
      size: String,
      // 圖標類名
      icon: {
        type: String,
        default: ''
      },
      // 原生type屬性 button / submit / reset
      nativeType: {
        type: String,
        default: 'button'
      },
      // 是否加載中狀態
      loading: Boolean,
      // 是否禁用狀態
      disabled: Boolean,
      // 是否樸素按鈕
      plain: Boolean,
      // 是否默認聚焦
      autofocus: Boolean,
      // 是否圓形按鈕
      round: Boolean
    },

    computed: {
      // elFormItem 尺寸獲取
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      // 按鈕尺寸計算
      buttonSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      }
    },

    methods: {
      // 點擊事件,使得組件的點擊事件為 @click,與原生點擊保持一致。
      handleClick(evt) {
        this.$emit('click', evt);
      }
    }
  };
</script>

代碼已注釋~其實 script 部分很簡單:通過inject和props獲取數據,計算方法計算尺寸,事件處理點擊事件。關鍵點在于button中的幾個class。說白了,其實這就是個原生的button組件,只是樣式上有所不同。
樣式文件都是存在 packages/theme-chalk/ 目錄下的。所以 button 的樣式目錄位于 packages/theme-chalk/src/button.scss。由于自己 CSS 非常渣,所以這步先跳過~

Radio

來看下Radio。嗯……目錄位置在 packages/radio/ 目錄下。index.js 文件和 button 是一樣的 —— 導入組件、定義全局注冊組件方法、導出。所以這邊來看看 radio.vue 文件。

<!-- packages/button/src/radio.vue -->
<template>
  <label
    class="el-radio"
    :class="[
      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 = label"
  >
    <!-- radio 圖標部分 -->
    <span class="el-radio__input"
      :class="{
        'is-disabled': isDisabled,
        'is-checked': model === label
      }"
    > 
      <!-- 圖標效果 -->
      <span class="el-radio__inner"></span>
      <input
        class="el-radio__original"
        :value="label"
        type="radio"
        v-model="model"
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :disabled="isDisabled"
        tabindex="-1"
      >
    </span>
    <!-- 文本內容 -->
    <span class="el-radio__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: 'ElRadio',
    // 混合選項
    mixins: [Emitter],

    inject: {
      elForm: {
        default: ''
      },

      elFormItem: {
        default: ''
      }
    },

    componentName: 'ElRadio',

    props: {
      // value值
      value: {},
      // Radio 的 value
      label: {},
      // 是否禁用
      disabled: Boolean,
      // 原生 name 屬性
      name: String,
      // 是否顯示邊框
      border: Boolean,
      // Radio 的尺寸,僅在 border 為真時有效 medium / small / mini
      size: String
    },

    data() {
      return {
        focus: false
      };
    },
    computed: {
      // 向上遍歷查詢父級組件是否有 ElRadioGroup,即是否在按鈕組中
      isGroup() {
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
          } else {
            this._radioGroup = parent;
            return true;
          }
        }
        return false;
      },
      // 重新定義 v-model 綁定內容的 get 和 set
      model: {
        get() {
          return this.isGroup ? this._radioGroup.value : this.value;
        },
        set(val) {
          if (this.isGroup) {
            this.dispatch('ElRadioGroup', 'input', [val]);
          } else {
            this.$emit('input', val);
          }
        }
      },
      // elFormItem 尺寸
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      // 計算 radio 尺寸,用于顯示帶有邊框的radio的尺寸大小
      radioSize() {
        const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        return this.isGroup
          ? this._radioGroup.radioGroupSize || temRadioSize
          : temRadioSize;
      },
      // 是否禁用,如果radioGroup禁用則按鈕禁用。
      isDisabled() {
        return this.isGroup
          ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
          : this.disabled || (this.elForm || {}).disabled;
      },
      // 標簽索引 0 or -1
      tabIndex() {
        return !this.isDisabled ? (this.isGroup ? (this.model === this.label ? 0 : -1) : 0) : -1;
      }
    },

    methods: {
      // 處理 @change 事件,如果有按鈕組,出發按鈕組事件。
      handleChange() {
        this.$nextTick(() => {
          this.$emit('change', this.model);
          this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
        });
      }
    }
  };
</script>

從代碼中可以看到,radio不止是個input那么簡單。后面還會顯示的文本內容。script 部分的邏輯是:通過 inject 和 props 獲取傳參,data記錄 radio 的 focus 狀態。compute 中計算一些屬性值;methods中處理onchange事件。radio 中多了些關于 radioGroup 的處理。具體我都寫在注釋中了。
另外要注意的還是 template 中的各類 class,radio的css文件想必也猜到了,文件位于 packages/theme-chalk/src/radio.scss到這里就會發現:這些簡單組件主要是樣式上面的處理,對于邏輯上處理并不大。
在 radio 中導入了 element-ui/src/mixins/emitter.js 文件,來看看它的作用是什么?

// src/mixins/emitter.js
// 廣播
function broadcast(componentName, eventName, params) {
  // 遍歷子組件
  this.$children.forEach(child => {
    // 組件名
    var name = child.$options.componentName;

    if (name === componentName) {
      // 觸發事件
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      // 執行broadcast方法
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
    dispatch(componentName, eventName, params) {
      // 父級組件及其組件名
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;

      // 有父級組件 同時 沒有name 或者 name 不等于組件名
      while (parent && (!name || name !== componentName)) {
        // parent 向上獲取父級組件
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      // 觸發 eventName 事件
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

由注釋可見,dispatch 方法向上獲取父級組件并觸發 eventName 事件。broadcast 方法向下遍歷子組件觸發 eventName 事件。

至于 Vue 的 mixins 屬性是干嘛的?

混入 (mixins) 是一種分發 Vue 組件中可復用功能的非常靈活的方式。混入對象可以包含任意組件選項。當組件使用混入對象時,所有混入對象的選項將被混入該組件本身的選項。

好啦,至于 radio 的 scss 解析?抱歉,暫時對scss不熟,之后補上~本篇關鍵將邏輯吧。

checkbox

找到 checkbox 的項目目錄,index.js 邏輯是一樣的~所以只需看看 checkbox.vue 文件

<!-- packages/button/src/checkbox.vue -->
<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"
  >
    <!-- checkbox -->
    <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>
      <!-- 如果有 true-value 或者 false-value -->
      <input
        v-if="trueLabel || falseLabel"
        class="el-checkbox__original"
        type="checkbox"
        :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"
        :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',
    // 混合 Emitter
    mixins: [Emitter],

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    componentName: 'ElCheckbox',

    data() {
      return {
        // checkbox model
        selfModel: false,
        // 焦點
        focus: false,
        // 超過限制?
        isLimitExceeded: false
      };
    },

    computed: {
      model: {
        // 獲取model值
        get() {
          return this.isGroup
            ? this.store : this.value !== undefined
              ? this.value : this.selfModel;
        },
        // 設置 selfModel
        set(val) {
          // checkbox group 的set邏輯處理
          if (this.isGroup) {
            // 處理 isLimitExceeded
            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));
            // 觸發 ElCheckboxGroup 的 input 事件
            this.isLimitExceeded === false &&
            this.dispatch('ElCheckboxGroup', 'input', [val]);
          } else {
            // 觸發當前組件 input 事件
            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;
      },
      // 判斷 group,checkbox 的 value 獲取
      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;
      },
      // elFormItem 的尺寸
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      // checkbox 尺寸,同樣需要有邊框才有效
      checkboxSize() {
        const temCheckboxSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        return this.isGroup
          ? this._checkboxGroup.checkboxGroupSize || temCheckboxSize
          : temCheckboxSize;
      }
    },

    props: {
      // value值
      value: {},
      // 選中狀態的值(只有在checkbox-group或者綁定對象類型為array時有效)
      label: {},
      // 設置 indeterminate 狀態,只負責樣式控制
      indeterminate: Boolean,
      // 是否禁用
      disabled: Boolean,
      // 當前是否勾選
      checked: Boolean,
      // 原生 name 屬性
      name: String,
      // 選中時的值
      trueLabel: [String, Number],
      // 沒有選中時的值
      falseLabel: [String, Number],
      id: String, /* 當indeterminate為真時,為controls提供相關連的checkbox的id,表明元素間的控制關系*/
      controls: String, /* 當indeterminate為真時,為controls提供相關連的checkbox的id,表明元素間的控制關系*/
      // 是否顯示邊框
      border: Boolean,
      // Checkbox 的尺寸,僅在 border 為真時有效
      size: String
    },

    methods: {
      // 添加數據到model
      addToStore() {
        if (
          Array.isArray(this.model) &&
          this.model.indexOf(this.label) === -1
        ) {
          this.model.push(this.label);
        } else {
          this.model = this.trueLabel || true;
        }
      },
      // 處理 @change 事件,如果是 group 要處理 group 的 change 事件。
      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() {
      // 如果 checked 為 true,執行 addToStore 方法
      this.checked && this.addToStore();
    },
    mounted() { // 為indeterminate元素 添加aria-controls 屬性
      if (this.indeterminate) {
        this.$el.setAttribute('aria-controls', this.controls);
      }
    }
  };
</script>

其實和radio邏輯差不多。從上面的內容中已知 emitter.js 用于觸發子組件或父組件的事件,而參數上與radio也差不多。不同點有,在顯示checkbox時,如果有true-babel 或 false-babel 屬性和沒有這兩個屬性顯示的是不同的 checkbox(v-if,v-else)。其他都差不多。
具體代碼注釋即可。

InputNumber

<template>
  <div
    @dragstart.prevent
    :class="[
      'el-input-number',
      inputNumberSize ? 'el-input-number--' + inputNumberSize : '',
      { 'is-disabled': inputNumberDisabled },
      { 'is-without-controls': !controls },
      { 'is-controls-right': controlsAtRight }
    ]">
    <!-- 減法 -->
    <span
      class="el-input-number__decrease"
      role="button"
      v-if="controls"
      v-repeat-click="decrease"
      :class="{'is-disabled': minDisabled}"
      @keydown.enter="decrease">
      <i :class="`el-icon-${controlsAtRight ? 'arrow-down' : 'minus'}`"></i>
    </span>
    <!-- 加法 -->
    <span
      class="el-input-number__increase"
      role="button"
      v-if="controls"
      v-repeat-click="increase"
      :class="{'is-disabled': maxDisabled}"
      @keydown.enter="increase">
      <i :class="`el-icon-${controlsAtRight ? 'arrow-up' : 'plus'}`"></i>
    </span>
    <!-- el-input 內容 -->
    <el-input
      ref="input"
      :value="currentValue"
      :disabled="inputNumberDisabled"
      :size="inputNumberSize"
      :max="max"
      :min="min"
      :name="name"
      :label="label"
      @keydown.up.native.prevent="increase"
      @keydown.down.native.prevent="decrease"
      @blur="handleBlur"
      @focus="handleFocus"
      @change="handleInputChange">
      <!-- 占位符模板 -->
      <template slot="prepend" v-if="$slots.prepend">
        <slot name="prepend"></slot>
      </template>
      <template slot="append" v-if="$slots.append">
        <slot name="append"></slot>
      </template>
    </el-input>
  </div>
</template>
<script>
  import ElInput from 'element-ui/packages/input';
  import Focus from 'element-ui/src/mixins/focus';
  import RepeatClick from 'element-ui/src/directives/repeat-click';

  export default {
    name: 'ElInputNumber',
    // options 混合
    mixins: [Focus('input')],
    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },
    // 自定義指令
    directives: {
      repeatClick: RepeatClick
    },
    components: {
      ElInput
    },
    props: {
      // 計數器步長
      step: {
        type: Number,
        default: 1
      },
      // 設置計數器允許的最大值    
      max: {
        type: Number,
        default: Infinity
      },
      // 設置計數器允許的最小值
      min: {
        type: Number,
        default: -Infinity
      },
      // 綁定值
      value: {},
      // 是否禁用計數器
      disabled: Boolean,
      // 計數器尺寸 large, small
      size: String,
      // 是否使用控制按鈕
      controls: {
        type: Boolean,
        default: true
      },
      // 控制按鈕位置 right
      controlsPosition: {
        type: String,
        default: ''
      },
      // 原生 name 屬性
      name: String,
      // 輸入框關聯的label文字
      label: String
    },
    data() {
      return {
        // 當前值
        currentValue: 0
      };
    },
    watch: {
      value: {
        // 立即執行 get()
        immediate: true,
        handler(value) {
          let newVal = value === undefined ? value : Number(value);
          if (newVal !== undefined && isNaN(newVal)) return;
          if (newVal >= this.max) newVal = this.max;
          if (newVal <= this.min) newVal = this.min;
          this.currentValue = newVal;
          // 觸發 @input 事件
          this.$emit('input', newVal);
        }
      }
    },
    computed: {
      // 最小禁用,無法再減
      minDisabled() {
        return this._decrease(this.value, this.step) < this.min;
      },
      // 最大禁用,無法再加
      maxDisabled() {
        return this._increase(this.value, this.step) > this.max;
      },
      // 精度
      precision() {
        const { value, step, getPrecision } = this;
        return Math.max(getPrecision(value), getPrecision(step));
      },
      // 按鈕是否要顯示于右側
      controlsAtRight() {
        return this.controlsPosition === 'right';
      },
      // FormItem尺寸
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      // 計算尺寸
      inputNumberSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      // 獲取禁用狀態
      inputNumberDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      }
    },
    methods: {
      // 計算精度
      toPrecision(num, precision) {
        if (precision === undefined) precision = this.precision;
        return parseFloat(parseFloat(Number(num).toFixed(precision)));
      },
      // 獲取精度
      getPrecision(value) {
        if (value === undefined) return 0;
        const valueString = value.toString();
        const dotPosition = valueString.indexOf('.');
        let precision = 0;
        if (dotPosition !== -1) {
          precision = valueString.length - dotPosition - 1;
        }
        return precision;
      },
      // 獲取加法后的精度
      _increase(val, step) {
        if (typeof val !== 'number' && val !== undefined) return this.currentValue;

        const precisionFactor = Math.pow(10, this.precision);
        // Solve the accuracy problem of JS decimal calculation by converting the value to integer.
        return this.toPrecision((precisionFactor * val + precisionFactor * step) / precisionFactor);
      },
      // 獲取減法后的精度
      _decrease(val, step) {
        if (typeof val !== 'number' && val !== undefined) return this.currentValue;

        const precisionFactor = Math.pow(10, this.precision);

        return this.toPrecision((precisionFactor * val - precisionFactor * step) / precisionFactor);
      },
      // 加法行為
      increase() {
        if (this.inputNumberDisabled || this.maxDisabled) return;
        const value = this.value || 0;
        const newVal = this._increase(value, this.step);
        this.setCurrentValue(newVal);
      },
      // 減法行為
      decrease() {
        if (this.inputNumberDisabled || this.minDisabled) return;
        const value = this.value || 0;
        const newVal = this._decrease(value, this.step);
        this.setCurrentValue(newVal);
      },
      // 處理 blur 和 focus
      handleBlur(event) {
        this.$emit('blur', event);
        this.$refs.input.setCurrentValue(this.currentValue);
      },
      handleFocus(event) {
        this.$emit('focus', event);
      },
      // 設置當前value
      setCurrentValue(newVal) {
        const oldVal = this.currentValue;
        if (newVal >= this.max) newVal = this.max;
        if (newVal <= this.min) newVal = this.min;
        if (oldVal === newVal) {
          // 執行 el-input 中的 setCurrentValue 方法
          this.$refs.input.setCurrentValue(this.currentValue);
          return;
        }
        // 觸發事件,改變value
        this.$emit('change', newVal, oldVal);
        this.$emit('input', newVal);
        this.currentValue = newVal;
      },
      // 處理文本框變化
      handleInputChange(value) {
        const newVal = value === '' ? undefined : Number(value);
        if (!isNaN(newVal) || value === '') {
          this.setCurrentValue(newVal);
        }
      }
    },
    mounted() {
      // 更改 el-input 內部 input 的屬性。
      let innerInput = this.$refs.input.$refs.input;
      innerInput.setAttribute('role', 'spinbutton');
      innerInput.setAttribute('aria-valuemax', this.max);
      innerInput.setAttribute('aria-valuemin', this.min);
      innerInput.setAttribute('aria-valuenow', this.currentValue);
      innerInput.setAttribute('aria-disabled', this.inputNumberDisabled);
    },
    updated() {
      // 更改 el-input 內部 input 的屬性。
      let innerInput = this.$refs.input.$refs.input;
      innerInput.setAttribute('aria-valuenow', this.currentValue);
    }
  };
</script>

先看HTML,這個組件由兩個span按鈕當做加減按鈕,一個 el-input 組件顯示數字結果。但是,對于最后兩個 template 的作用不是很明白,不知道何時使用。
再看JS,代碼中導入了 el-input 組件、 Focus 方法和 RepeatClick 方法。這兩個方法之后詳述。看到代碼中還是以props、inject、compute方法來處理傳入的數據。和前三個組件不同的地方在于,一是引用了組件需要處理組件,在 mounted 和 updated 方法執行時修改 el-input 組件中 input 的屬性;二是加入了加法和減法的方法邏輯:求精度、做加減法、處理最大最小值限制。
從CSS角度上來說,要處理加法、減法按鈕的位置和樣式等功能……SCSS方面的內容暫且一概不論。

順便看看 Focus 和 RepeatClick 方法~

// src/directives/repeat-click.js
import { once, on } from 'element-ui/src/utils/dom';
// on 添加監聽事件
// once 監聽一次事件

export default {
  bind(el, binding, vnode) {
    let interval = null;
    let startTime;
    // 執行表達式方法
    const handler = () => vnode.context[binding.expression].apply();
    // 清除interval
    const clear = () => {
      if (new Date() - startTime < 100) {
        handler();
      }
      clearInterval(interval);
      interval = null;
    };

    // 監聽 mousedown 鼠標點擊事件
    on(el, 'mousedown', (e) => {
      if (e.button !== 0) return;
      startTime = new Date();
      once(document, 'mouseup', clear);
      clearInterval(interval);
      // setInterval() 方法可按照指定的周期(以毫秒計)來調用函數或計算表達式。
      interval = setInterval(handler, 100);
    });
  }
};

其中導入的on和once方法類似于vue的方法,on用于監聽事件,once監聽一次事件。監聽 mousedown 事件。如果觸發每 100 毫秒執行一次 handler,如果監聽到 mousedown 判斷時間間隔如果短于100毫秒,執行 handler 方法。取消計時器并清空 interval。從而實現了重復點擊的效果。

// src/mixins/focus.js
export default function(ref) {
  return {
    methods: {
      focus() {
        this.$refs[ref].focus();
      }
    }
  };
};

很簡單的方法,就是用于定位一個元素,執行它的 focus 方法。

所以說:src 中除了 locale 目錄用于國際化和 utils 工具目錄外,其他目錄下都是用于組件的一些配置上的.如 mixins 目錄用于 mixins 項,directives 目錄下的用于 directives 項。

最后

額……由于對于 CSS 比較菜,所以暫且不誤人子弟,待我學成后再補上這部分內容。
總結下幾個組件給我的感覺:其實 UI 框架主要在 UI 二字上。所以其實講 UI 框架重點應該放在 CSS 上。邏輯部分其實蠻簡單的。畢竟是組件不是整個項目,組件就是要易用性、可擴展性。
所以說組件的主要邏輯都是進行傳參的處理和計算。
element的組件模式上其實是定義好 [component].vue 組件文件,然后導入到用戶項目 Vue 實例的 components 項中來使用,其實和自定義 component 基本原理差不多。
下一篇,來看看一些復雜的組件的代碼邏輯實現~~

打個廣告

上海鏈家-鏈家上海研發中心需求大量前端、后端、測試,需要內推請將簡歷發送至 dingxiaojie001@ke.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容