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绑定事件

(本文完)

相关推荐
慧一居士13 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead15 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js