前言
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第38期,链接:juejin.cn/post/713472…
Stepper组件了解
步进器由增加按钮、减少按钮和输入框组成,用于在一定范围内输入、调整数字
Stepper组件官方文档:vant.pro/vant-weapp/…
组件UI截图:
组件结构
index.wxml
minus按钮 + input输入框 + plus按钮页面UI代码
<wxs src="../wxs/utils.wxs" module="utils" />
<wxs src="./index.wxs" module="computed" />
<view class="{{ utils.bem('stepper', [theme]) }} custom-class">
<view
wx:if="{{ showMinus }}"
data-type="minus"
style="{{ computed.buttonStyle({ buttonSize }) }}"
class="minus-class {{ utils.bem('stepper__minus', { disabled: disabled || disableMinus || currentValue <= min }) }}"
hover-class="van-stepper__minus--hover"
hover-stay-time="70"
bind:tap="onTap"
bind:touchstart="onTouchStart"
bind:touchend="onTouchEnd"
>
<slot name="minus" />
</view>
<input
type="{{ integer ? 'number' : 'digit' }}"
class="input-class {{ utils.bem('stepper__input', { disabled: disabled || disableInput }) }}"
style="{{ computed.inputStyle({ buttonSize, inputWidth }) }}"
value="{{ currentValue }}"
focus="{{ focus }}"
disabled="{{ disabled || disableInput }}"
always-embed="{{ alwaysEmbed }}"
bindinput="onInput"
bind:focus="onFocus"
bind:blur="onBlur"
/>
<view
wx:if="{{ showPlus }}"
data-type="plus"
style="{{ computed.buttonStyle({ buttonSize }) }}"
class="plus-class {{ utils.bem('stepper__plus', { disabled: disabled || disablePlus || currentValue >= max }) }}"
hover-class="van-stepper__plus--hover"
hover-stay-time="70"
bind:tap="onTap"
bind:touchstart="onTouchStart"
bind:touchend="onTouchEnd"
>
<slot name="plus" />
</view>
</view>
index.wxss
组件样式
index.json
标识组件
{
"component": true
}
index.ts
组件的属性设置
组件的功能逻辑实现
import { VantComponent } from '../common/component';
import { isDef } from '../common/validator';
VantComponent({
...
},
data: {
currentValue: '',
},
watch: {
...
},
created() {
...
},
methods: {
...
},
});
源码详解
1. 组件初始化
- VantComponent函数
将vantOptions数据整合成微信小程序Component所需要的传参数据,然后调用微信小程序Component类来创建组件
function VantComponent<
Data extends WechatMiniprogram.Component.DataOption,
Props extends WechatMiniprogram.Component.PropertyOption,
Methods extends WechatMiniprogram.Component.MethodOption
>(vantOptions: VantComponentOptions<Data, Props, Methods>): void {
const options: WechatMiniprogram.Component.Options<Data, Props, Methods> = {};
// 整合vantOptions里面的数据到options中
mapKeys(vantOptions, options, {
data: 'data',
props: 'properties',
watch: 'observers',
mixins: 'behaviors',
methods: 'methods',
beforeCreate: 'created',
created: 'attached',
mounted: 'ready',
destroyed: 'detached',
classes: 'externalClasses',
});
// add default externalClasses
options.externalClasses = options.externalClasses || [];
options.externalClasses.push('custom-class');
// add default behaviors
options.behaviors = options.behaviors || [];
options.behaviors.push(basic);
// add relations
const { relation } = vantOptions;
if (relation) {
options.relations = relation.relations;
options.behaviors.push(relation.mixin);
}
// map field to form-field behavior
if (vantOptions.field) {
options.behaviors.push('wx://form-field');
}
// add default options
options.options = {
multipleSlots: true,
addGlobalClass: true,
};
Component(options);
}
export { VantComponent };
- mapKeys函数
将source对象的属性值赋值给target对象,实现对象的浅拷贝
function mapKeys(
source: Record<string, any>,
target: Record<string, any>,
map: Record<string, any>
) {
Object.keys(map).forEach((key) => {
if (source[key]) {
target[map[key]] = source[key];
}
});
}
- 组件初始化
- 在 Vant Weapp 的小程序组件中,
props和data中的属性都可以通过this.data统一访问 - Created钩子函数将外部传递的value赋值给当前组件属性currentValue
- observeValue函数在watch中使用,观察外部传入组件的value属性变化,并将变化值赋值给currentValue
- filter函数是过滤value中不是数字、点和小横线的字符
- format函数将value控制在min值至max值范围内,如果存在小数位限制,还要格式化成小数位的字符串
- 在 Vant Weapp 的小程序组件中,
// 导入VantComponent函数创建组件
import { VantComponent } from '../common/component';
import { isDef } from '../common/validator';
const LONG_PRESS_START_TIME = 600;
const LONG_PRESS_INTERVAL = 200;
// add num and avoid float number
function add(num1: number, num2: number) {
const cardinal = 10 ** 10;
return Math.round((num1 + num2) * cardinal) / cardinal;
}
function equal(value1: number | string, value2: number | string) {
return String(value1) === String(value2);
}
VantComponent({
field: true,
// 内置样式
classes: ['input-class', 'plus-class', 'minus-class'],
// 外部传参属性
props: {
value: { // 输入框value值
type: null,
},
integer: { // 是否为整数
type: Boolean,
observer: 'check',
},
disabled: Boolean, // 是否禁用
inputWidth: String, // input宽度
buttonSize: String, // 按钮尺寸
asyncChange: Boolean, // 如果需要异步地修改输入框的值,可以设置`async-change`属性,并在`change`事件中手动修改`value`
disableInput: Boolean, // 禁用输入框
decimalLength: { // 小数点位数长度
type: Number,
value: null as unknown as number,
observer: 'check',
},
min: { // 最小值
type: null,
value: 1,
observer: 'check',
},
max: { // 最大值
type: null,
value: Number.MAX_SAFE_INTEGER,
observer: 'check',
},
step: { // 步长
type: null,
value: 1,
},
showPlus: { // 显示加号按钮
type: Boolean,
value: true,
},
showMinus: { // 显示减号按钮
type: Boolean,
value: true,
},
disablePlus: Boolean, // 禁用加号按钮
disableMinus: Boolean, // 禁用减号按钮
longPress: { // 是否长按
type: Boolean,
value: true,
},
theme: String,
alwaysEmbed: Boolean,
},
data: {
currentValue: '',
},
watch: {
// 观察value属性的变化
value() {
this.observeValue();
},
},
created() {
// 将外部传入的value赋值给currentValue
this.setData({
currentValue: this.format(this.data.value).newValue,
});
},
methods: {
observeValue() {
const { value } = this.data;
this.setData({ currentValue: this.format(value).newValue });
},
filter(value: string): string {
// 替换除了数字、点和小横线的其他字符为空
value = String(value).replace(/[^0-9.-]/g, '');
// 如果integer属性为true,则取整数部分
if (this.data.integer && value.indexOf('.') !== -1) {
value = value.split('.')[0];
}
return value;
},
format(value: string) {
// 过滤非法字符串
const safeValue = this.filter(value);
// 获取value在min-max的取值,超过max或min则取值max或min
const rangeValue = Math.max(
Math.min(this.data.max, +safeValue),
this.data.min
);
// 格式化小数点位数
const newValue = isDef(this.data.decimalLength)
? rangeValue.toFixed(this.data.decimalLength)
: String(rangeValue);
return { value, newValue };
},
},
});
2. 点击减号按钮
- wxml
<view
wx:if="{{ showMinus }}"
data-type="minus"
style="{{ computed.buttonStyle({ buttonSize }) }}"
class="minus-class {{ utils.bem('stepper__minus', { disabled: disabled || disableMinus || currentValue <= min }) }}"
hover-class="van-stepper__minus--hover"
hover-stay-time="70"
bind:tap="onTap"
bind:touchstart="onTouchStart"
bind:touchend="onTouchEnd"
>
<slot name="minus" />
</view>
- event
- 点击按钮触发tap事件,这里会向外部父组件广播3个事件,overlimit事件、change事件和minus事件
- dataset能获取view上data-的所有自定义属性值
onTap(event) {
// 获取data-type属性minus
const { type } = event.currentTarget.dataset;
// 将minus赋值到this.type
this.type = type;
// 触发change
this.onChange();
}
onChange() {
// type为minus或者plus
const { type } = this;
// 如果判断是disable状态,则向外部广播一个overlimit事件
if (this.isDisabled(type)) {
this.$emit('overlimit', type);
return;
}
// 加减步长值
const diff = type === 'minus' ? -this.data.step : +this.data.step;
// 获取加减步长之后新的value
const value = this.format(String(add(+this.data.currentValue, diff)));
this.emitChange(value);
// 广播minus事件
this.$emit(type);
}
isDisabled(type) {
const { disabled, disablePlus, disableMinus, currentValue, max, min } = this.data;
if (type === 'plus') {
// 存在disable属性或者value超出max边界
return disabled || disablePlus || +currentValue >= +max;
}
// 存在disable属性或者value超出min边界
return disabled || disableMinus || +currentValue <= +min;
},
emitChange(data) {
const { value, newValue } = data;
if (!this.data.asyncChange) {
// fix when input 11. parsed to 11, unable to enter decimal
this.setData({ currentValue: +value === +newValue ? value : newValue });
}
// 广播change事件
this.$emit('change', +newValue);
},
3. 长按减号按钮
触发onTouchStart和onTouchEnd事件
onTouchStart(event) {
// 如果设置longPress为false, return
if (!this.data.longPress) {
return;
}
// 清除已经存在的定时器
clearTimeout(this.longPressTimer);
const { type } = event.currentTarget.dataset;
this.type = type;
this.isLongPress = false;
this.longPressTimer = setTimeout(() => {
this.isLongPress = true;
// 600ms后触发onChange事件
this.onChange();
this.longPressStep();
}, LONG_PRESS_START_TIME);
},
longPressStep() {
// longPressStep自己调用自己实现定时增加或者减少步长
this.longPressTimer = setTimeout(() => {
this.onChange();
this.longPressStep();
}, LONG_PRESS_INTERVAL);
},
onTouchEnd() {
if (!this.data.longPress) {
return;
}
// 清楚定时器
clearTimeout(this.longPressTimer);
},
4. 点击加号按钮和长按加号按钮
除了配置属性命名不一致外,功能和减号按钮都一致
<view
wx:if="{{ showPlus }}"
data-type="plus"
style="{{ computed.buttonStyle({ buttonSize }) }}"
class="plus-class {{ utils.bem('stepper__plus', { disabled: disabled || disablePlus || currentValue >= max }) }}"
hover-class="van-stepper__plus--hover"
hover-stay-time="70"
bind:tap="onTap"
bind:touchstart="onTouchStart"
bind:touchend="onTouchEnd"
>
<slot name="plus" />
</view>
5. 输入框事件
<input
type="{{ integer ? 'number' : 'digit' }}"
class="input-class {{ utils.bem('stepper__input', { disabled: disabled || disableInput }) }}"
style="{{ computed.inputStyle({ buttonSize, inputWidth }) }}"
value="{{ currentValue }}"
focus="{{ focus }}"
disabled="{{ disabled || disableInput }}"
always-embed="{{ alwaysEmbed }}"
bindinput="onInput"
bind:focus="onFocus"
bind:blur="onBlur"
/>
- onInput 获取wx提供的event对象的value, 向外部广播change事件
onInput(event) {
const { value = '' } = event.detail || {};
// allow input to be empty
if (value === '') {
return;
}
const formatted = this.format(value);
this.emitChange(formatted);
},
- onFocus 获取event对象的detail数据,广播focus事件到外层组件
onFocus(event) {
this.$emit('focus', event.detail);
}
- onBlur 格式化event.detail.value数据然后广播chang事件和blur事件到外层组件
onBlur(event) {
const data = this.format(event.detail.value);
this.setData({ currentValue: data.newValue });
this.emitChange(data);
this.$emit('blur', Object.assign(Object.assign({}, event.detail), { value: +data.newValue }));
}
总结
从组件的wxml、css和JS了解一个stepp组件的实现逻辑,组件功能相对简单。
坚持阅读源码,如有问题,欢迎指正