elment-ui组件源码分析学习(2)

本次一共也是20个组件

实际是11个标准功能组件,归为普通类normal

笔者分析过程是将element-ui组件重新抽出来,可以理解为自己写一套组件库,可以看到第一个图里面,笔者是以unit也就是单元为开头的。

另外笔者发现,将组件按照简单、普通、困难划分并不是一个好的想法,笔者之前在《技术并不一定比其他高级》文章里写到

要想尽快搞清楚技术,只要找到其中代表性的重复单元就可以了。而实际也早就有人总结了这些单元,比如功能单元的代表各种ui组件库,业务单元的代表往往是对功能单元的再加工。好比功能单元是原型机,而业务单元是定制化。

虽然ui组件库是功能单元,但也是有业务倾向的,相对笔者简单地将组件划分为简单、普通、困难element-ui原有的分类更好。官网将element-ui分为Basic、Form、Data、Notice、Navigation和Others。

笔者又参考了ant-design的划分:通用、布局、导航、数据录入、数据展示、反馈、其他。

然后Vant-ui划分:基础组件、表单组件、反馈组件、展示组件、导航组件、业务组件。

笔者觉得ant-design和Vant-ui更好一些。组件分类应该更贴近业务或者贴近具体使用场景,这样便于记忆。考虑到具体开发过程中,根本上是将业务转为代码,命名贴近业务也能让前端开发程序员逐渐靠近业务。

后面会将分析学习的组件重新按照业务类别分类。

组件分析学习

布局组件

Row(el-row)是布局组件,源码

js 复制代码
<script>
export default {
  name: 'UnitRow',

  componentName: 'UnitRow',

  props: {
    tag: {
      type: String,
      default: 'div'
    },
    gutter: Number,
    type: String,
    justify: {
      type: String,
      default: 'start'
    },
    align: String
  },

  computed: {
    style() {
      const ret = {};

      if (this.gutter) {
        ret.marginLeft = `-${this.gutter / 2}px`;
        ret.marginRight = ret.marginLeft;
      }

      return ret;
    }
  },

  render(h) {
    return h(this.tag, {
      class: [
        'el-row',
        this.justify !== 'start' ? `is-justify-${this.justify}` : '',
        this.align ? `is-align-${this.align}` : '',
        { 'el-row--flex': this.type === 'flex' }
      ],
      style: this.style
    }, this.$slots.default);
  }
};

</script>

Row一般和Col一起使用,二者均是通过render函数写的模板,从Col源码中可以看出

js 复制代码
   computed: {
      // 说明el-col是可以单独用的,只不过这个时候的gutter是0
      gutter() {
        let parent = this.$parent;
        while (parent && parent.$options.componentName !== 'ElRow') {
          parent = parent.$parent;
        }
        return parent ? parent.gutter : 0;
      }
    },

Col(el-col)是可以单独使用的。

表单组件

本次表单组件有:Radio(el-radio)、Checkbox(el-checkbox)、Switch(el-switch)、Rate(el-rate)组件。

通过设置el-form属性设置表单组件

element-ui的表单自组件Radio、Checkbox、Switch等都会注入一个elForm,有的还会注入elFormItem,这样的开发的目的是通过设置elForm属性设置子组件的大小、是否禁用等。

比如Switch源码

js 复制代码
   computed: {
        ...
        switchDisabled() {
          return this.disabled || (this.elForm || {}).disabled;
        }
      },

比如Radio

js 复制代码
   computed: {
        _elFormItemSize() {
          return (this.elFormItem || {}).elFormItemSize;
        },
        radioSize() {
          const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
          return this.isGroup
            ? this._radioGroup.radioGroupSize || temRadioSize
            : temRadioSize;
        }
      },
通过dispatch派发跨组件的通信

不同于Vue项目将Vue挂载在一个全局变量上,再通过 <math xmlns="http://www.w3.org/1998/Math/MathML"> o n 、 on、 </math>on、emit、$off进行跨组件通信。element-ui跨组件通信源码

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.apply(child, [componentName, eventName].concat([params]));
      }
    });
  }
  export default {
    methods: {
      dispatch(componentName, eventName, params) {
        var parent = this.$parent || this.$root;
        var name = parent.$options.componentName;
        
        // 递归寻找到名字为componentName的组件
        while (parent && (!name || name !== componentName)) {
          parent = parent.$parent;
  
          if (parent) {
            name = parent.$options.componentName;
          }
        }
        if (parent) {
          parent.$emit.apply(parent, [eventName].concat(params));
        }
      },
      broadcast(componentName, eventName, params) {
        broadcast.call(this, componentName, eventName, params);
      }
    }
  };
  

具体使用

js 复制代码
watch: {
        value(value) {
          this.dispatch('UnitFormItem', 'el.form.change', value);
        }
      }
js 复制代码
 methods: {
        handleChange() {
          this.$nextTick(() => {
            this.$emit('change', this.model);
            this.isGroup && this.dispatch('UnitRadioGroup', 'handleChange', this.model);
          });
        }
      }

element-ui采用的是向上寻找,寻找到命名为componentName的父组件,然后再$emit事件。当然在父组件里面已经$on了某个事件。

~的使用

在Switch组件中有~的使用

js 复制代码
 created() {
        if (!~[this.activeValue, this.inactiveValue].indexOf(this.value)) {
          this.$emit('input', this.inactiveValue);
        }
      },

~是一个位运算符,被称为"按位非"运算符。它的作用是对操作数的每一位执行非运算。

~运算符在JavaScript中的一个常见用途是用来检查一个元素是否在数组中。因为indexOf方法会返回元素在数组中的索引,或者在元素不在数组中时返回-1,所以可以使用~运算符来检查indexOf的结果

javascript 复制代码
let array = [1, 2, 3, 4, 5];
let index = array.indexOf(3);

if (~index) {
  console.log('元素在数组中');
} else {
  console.log('元素不在数组中');
}

数据展示组件

本次数据展示组件:Empty(el-empty)、Result(el-result)、Statistic(el-statistic)、Timeline(el-timeline)、Descriptions(el-descriptions)、Collapse(el-collapse)

Empty是一个svg绘制的图片。Result是svg绘制的只不过是4个。

使用钩子函数销毁定时器

Statistic组件使用hook:beforeDestroy销毁定时器,避免了选项式Api总是分开写逻辑的问题

js 复制代码
      // 倒计时
      countDown(timeVlaue) {
        let {REFRESH_INTERVAL, timeTask, diffDate, formatTimeStr, stopTime, suspend } = this;
        if (timeTask) return;
        let than = this;
        this.timeTask = setInterval(()=> {
          let diffTiem = diffDate(timeVlaue, Date.now());
          than.disposeValue = formatTimeStr(diffTiem);
          stopTime(diffTiem);
        }, REFRESH_INTERVAL);
        // 销毁定时器
        this.$once('hook:beforeDestroy', () => {
          suspend(true);
        });
  
      }
this.$slots.default是数组

在Vue中,this.$slots对象的每个属性都对应一个插槽,属性的名字就是插槽的名字。每个插槽都是一个包含了该插槽中所有VNode(虚拟节点)的数组

Ctor是构造函数

在Descriptions组件中有段代码

js 复制代码
      const children = ((this.$slots.default || []).filter(vnode => vnode.tag &&
            vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'UnitDescriptionsItem'));
      const nodes = children.map(vnode => {
        return {
          props: this.getOptionProps(vnode),
          slots: this.getSlots(vnode),
          vnode
        };
      });

其中vnode.componentOptions.Ctor.options.name === 'UnitDescriptionsItem'vnode.componentOptions.Ctor中的Ctor是构造函数。vnode.componentOptions.Ctor就是这个组件的构造函数。

动态写transition绑定事件

Collapse组件的子组件CollapseItem有源码

js 复制代码
import { addClass, removeClass } from '../utils/dom';

class Transition {
  beforeEnter(el) {
    addClass(el, 'collapse-transition');
    if (!el.dataset) el.dataset = {};

    el.dataset.oldPaddingTop = el.style.paddingTop;
    el.dataset.oldPaddingBottom = el.style.paddingBottom;

    el.style.height = '0';
    el.style.paddingTop = 0;
    el.style.paddingBottom = 0;
  }

  enter(el) {
    el.dataset.oldOverflow = el.style.overflow;
    if (el.scrollHeight !== 0) {
      el.style.height = el.scrollHeight + 'px';
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    } else {
      el.style.height = '';
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    }

    el.style.overflow = 'hidden';
  }

  afterEnter(el) {
    // for safari: remove class then reset height is necessary
    removeClass(el, 'collapse-transition');
    el.style.height = '';
    el.style.overflow = el.dataset.oldOverflow;
  }

  beforeLeave(el) {
    if (!el.dataset) el.dataset = {};
    el.dataset.oldPaddingTop = el.style.paddingTop;
    el.dataset.oldPaddingBottom = el.style.paddingBottom;
    el.dataset.oldOverflow = el.style.overflow;

    el.style.height = el.scrollHeight + 'px';
    el.style.overflow = 'hidden';
  }

  leave(el) {
    if (el.scrollHeight !== 0) {
      // for safari: add class after set height, or it will jump to zero height suddenly, weired
      addClass(el, 'collapse-transition');
      el.style.height = 0;
      el.style.paddingTop = 0;
      el.style.paddingBottom = 0;
    }
  }

  afterLeave(el) {
    removeClass(el, 'collapse-transition');
    el.style.height = '';
    el.style.overflow = el.dataset.oldOverflow;
    el.style.paddingTop = el.dataset.oldPaddingTop;
    el.style.paddingBottom = el.dataset.oldPaddingBottom;
  }
}

export default {
  name: 'UnitCollapseTransition',
  functional: true,
  render(h, { children }) {
    const data = {
      on: new Transition()
    };

    return h('transition', data, children);
  }
};

这是动态给transition绑定事件。原来也可以这样写组件绑定事件。

总结一下

  1. 学习将功能组件按照业务倾向进行业务类型划分
  2. 表单组件通过provide和inject实现设置el-form,设置子表单组件属性
  3. element-ui源码使用向上寻找指定父组件的方式$emit事件
  4. ~用来判断数组是否含有某个值
  5. 重温了一下使用钩子函数销毁定时器
  6. this.$slots上的插槽模式是Vnode的数组
  7. vnode.componentOptions.Ctor就是这个组件的构造函数
  8. 动态给transition绑定事件

(本文完)

相关推荐
SsunmdayKT12 小时前
前后端项目部署与运行机制全流程详解
前端·后端
本末倒置18312 小时前
Vue 3 开发者转型 React 指南:保姆级教程
前端·javascript·vue.js
Reart12 小时前
从0解构tinyWeb项目--(Day:10)
前端·后端·架构
牛蛙点点申请出战13 小时前
IconFontViewer -- 一个可以在 Android Studio 中实时预览 IconFont 的插件
android·前端·intellij idea
空中海13 小时前
03 渲染机制、性能优化与现代 React
javascript·react.js·性能优化
ChalesXavier13 小时前
Fetch API 的基本用法
javascript
是上好佳佳佳呀13 小时前
【前端(十三)】JavaScript 数组与字符串笔记
前端·javascript·笔记
巴沟旮旯儿13 小时前
vite项目配置文件和打包
前端·设计模式
彩票管理中心秘书长13 小时前
Pinia 插件架构与组合式函数:如何让你的 Store 长出“超能力”
前端