- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第38期,链接:juejin.cn/post/713472…
Stepper
首先我运行小程序模式时是出现了问题的,有报错,错误如下:
然后我使用vscode看了一下:
这句报错了,然后我把devtools直接改成了windows,然后就可以npm run dev成功了,在example下的dist就生成成功了。
然后依照步骤在微信开发工具中就可以看到如下界面
由于是一个组件,源码分为三个部门即wxml(布局), js, wxss(样式,不进行叙述): wxml:
<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
always-embed="{{ false }}"
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>
从上面可以看出view和html使用的div基本是无区别的,写法也是和vue中的页面写法非常相似(本人只学习了vue,对于其他不熟,所以基本只会针对vue进行对比)。 其中包含两个view,一个input,两个view是步进器前后的加减按钮,input是中间的数字框,而且针对加减按钮添加了name为plus和minus的slot,可以对两个按钮进行扩展(此在官方文档中没有提及)。
js:
import { VantComponent } from '../common/component';
import { isDef } from '../common/validator';
const LONG_PRESS_START_TIME = 600;
const LONG_PRESS_INTERVAL = 200;
function add(num1, num2) {
const cardinal = Math.pow(10, 10);
return Math.round((num1 + num2) * cardinal) / cardinal;
}
function equal(value1, value2) {
return String(value1) === String(value2);
}
VantComponent({
field: true,
classes: ['input-class', 'plus-class', 'minus-class'],
props: {
value: {
type: null,
observer: 'observeValue',
},
integer: {
type: Boolean,
observer: 'check',
},
disabled: Boolean,
inputWidth: String,
buttonSize: String,
asyncChange: Boolean,
disableInput: Boolean,
decimalLength: {
type: Number,
value: null,
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: '',
},
created() {
this.setData({
currentValue: this.format(this.data.value),
});
},
methods: {
observeValue() {
const { value, currentValue } = this.data;
if (!equal(value, currentValue)) {
this.setData({ currentValue: this.format(value) });
}
},
check() {
const val = this.format(this.data.currentValue);
if (!equal(val, this.data.currentValue)) {
this.setData({ currentValue: val });
}
},
isDisabled(type) {
const { disabled, disablePlus, disableMinus, currentValue, max, min, } = this.data;
if (type === 'plus') {
return disabled || disablePlus || currentValue >= max;
}
return disabled || disableMinus || currentValue <= min;
},
onFocus(event) {
this.$emit('focus', event.detail);
},
onBlur(event) {
const value = this.format(event.detail.value);
this.emitChange(value);
this.$emit('blur', Object.assign(Object.assign({}, event.detail), { value }));
},
// filter illegal characters
filter(value) {
value = String(value).replace(/[^0-9.-]/g, '');
if (this.data.integer && value.indexOf('.') !== -1) {
value = value.split('.')[0];
}
return value;
},
// limit value range
format(value) {
value = this.filter(value);
// format range
value = value === '' ? 0 : +value;
value = Math.max(Math.min(this.data.max, value), this.data.min);
// format decimal
if (isDef(this.data.decimalLength)) {
value = value.toFixed(this.data.decimalLength);
}
return value;
},
onInput(event) {
const { value = '' } = event.detail || {};
// allow input to be empty
if (value === '') {
return;
}
let formatted = this.filter(value);
// limit max decimal length
if (isDef(this.data.decimalLength) && formatted.indexOf('.') !== -1) {
const pair = formatted.split('.');
formatted = `${pair[0]}.${pair[1].slice(0, this.data.decimalLength)}`;
}
this.emitChange(formatted);
},
emitChange(value) {
if (!this.data.asyncChange) {
this.setData({ currentValue: value });
}
this.$emit('change', value);
},
onChange() {
const { type } = this;
if (this.isDisabled(type)) {
this.$emit('overlimit', type);
return;
}
const diff = type === 'minus' ? -this.data.step : +this.data.step;
const value = this.format(add(+this.data.currentValue, diff));
this.emitChange(value);
this.$emit(type);
},
longPressStep() {
this.longPressTimer = setTimeout(() => {
this.onChange();
this.longPressStep();
}, LONG_PRESS_INTERVAL);
},
onTap(event) {
const { type } = event.currentTarget.dataset;
this.type = type;
this.onChange();
},
onTouchStart(event) {
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;
this.onChange();
this.longPressStep();
}, LONG_PRESS_START_TIME);
},
onTouchEnd() {
if (!this.data.longPress) {
return;
}
clearTimeout(this.longPressTimer);
},
},
});
针对js的分析:
首先写法上是与vue2的版本非常的相似,基本无差别。
源码中针对0.1 + 0.2 = 0.30000000000000004的问题考虑的很细致,将此种情况进行了处理。(产生原因:JS 采用的是双精度版本, 即IEEE 754 双精度版本(64位),计算机的信息全部转化为二进制进行存储的,那么0.1的二进制表示的是一个无限循环小数,该版本的 JS 采用的是浮点数标准需要对这种无限循环的二进制进行截取,从而导致了精度丢失,造成了0.1不再是0.1,截取之后0.1变成了 0.100…001,0.2变成了0.200…002。所以两者相加的数大于0.3)。而且源码中针对值的比较都转换成了string类型的数据进行比较,不是针对number的数据进行比较(猜测应该是避免精度丢失的问题)。
- 开始初始化先进行了observeValue函数调用,给currentValue赋予初始值,再在created中对currentValue再一次进行了赋值。赋值时使用了format函数,format函数中调用了filter函数,将字符串中非数字的东西通过正则给过滤掉(严谨),并且会根据是否是整型来去除小数位(严谨)。
- 然后开始功能的讲解,最基础的数值的加减,主要通过onTap函数实现,首先根据event中获取按钮的类型,调用onChanage函数,onChange函数会根据按钮的状态判断是否触发overlimit事件,按钮的状态有可能虽然都是不可点击状态,但是有可能是设置disabled状态,或者单独针对一个按钮设置的状态,甚至是由于值超出范围引发的不可点击状态,然后根据步长计算input框的值,先调用emitChange函数触发change事件,在emitChange中还会判断是否需要异步设置值(没有搞懂这个异步设置的含义),在根据按钮类型触发对应的事件。
- 接着是针对input框的输入值的变化,调用的是onInput函数,onInput函数中调用了之前讲过的format函数与filter函数进行了值的清洗,然后使用了isDef函数校验了保留小数位这个参数是否存在,存在时进行了处理,最后调用了emitChange函数。
- 其他的功能包括长按,输入框聚焦/失焦等功能基本都是基于上述讲的三点进行,基本读完上述的三点,后续的功能基本也就了解。