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

(本文完)

相关推荐
书山有路_邓21 分钟前
vscode 如何鼠标双击时选择带有-的
前端
张三风啊2 小时前
vue config 接口地址配置
前端·javascript·vue.js
多情码农无情键2 小时前
浏览器漫谈HTML--2.2从表单标签看vue的响应式系统 理论+实战
前端·javascript·html
Uluoyu2 小时前
Vue.Draggable使用nested-with-vmodel进行拖拽
前端·javascript·vue.js
北极糊的狐2 小时前
vue页面成绩案例(for渲染表格/删除/添加/统计总分/平均分/不及格显红色/输入内容去首尾空格trim/输入内容转数字number)
前端·javascript·vue.js
边洛洛3 小时前
路由传参、搜索、多选框勾选、新增/编辑表单复用
前端·javascript·vue.js
breakthrough_014 小时前
创建一个简单的 Nuxt.js 应用
开发语言·javascript·ecmascript
OEC小胖胖5 小时前
Vue 3 中 onUnload 和 onPageScroll 使用详解
前端·javascript·vue.js·前端框架·web
秋田君6 小时前
uniapp中使用Mescroll实现下拉刷新与上拉加载项目实战
javascript·uni-app
川石教育6 小时前
Vue前端开发-slot传参
前端·vue.js·前端框架·前端开发·slot组件