本次一共也是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
绑定事件。原来也可以这样写组件绑定事件。
总结一下
- 学习将功能组件按照业务倾向进行业务类型划分
- 表单组件通过provide和inject实现设置el-form,设置子表单组件属性
- element-ui源码使用向上寻找指定父组件的方式
$emit
事件 ~
用来判断数组是否含有某个值- 重温了一下使用钩子函数销毁定时器
this.$slots
上的插槽模式是Vnode的数组vnode.componentOptions.Ctor
就是这个组件的构造函数- 动态给
transition
绑定事件
(本文完)