Vant Stepper 组件源码学习
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。这是源码共读的第38期,链接: 【若川视野 x 源码共读】第38期 | 经常用 vant-weapp 开发小程序,却不知道如何开发一个组件?学!
基础标签和事件
// 用到原生标签 button input button
// 对外暴露事件
emits: [
'plus',
'blur',
'minus',
'focus',
'change',
'overlimit',
'update:modelValue',
];
return () => (
<div role="group" class={bem([props.theme])}>
<button
v-show={props.showMinus}
type="button"
style={buttonStyle.value}
class={[
bem('minus', { disabled: minusDisabled.value }),
{ [HAPTICS_FEEDBACK]: !minusDisabled.value },
]}
aria-disabled={minusDisabled.value || undefined}
{...createListeners('minus')}
/>
<input
v-show={props.showInput}
ref={inputRef}
type={props.integer ? 'tel' : 'text'}
role="spinbutton"
class={bem('input')}
value={current.value}
style={inputStyle.value}
disabled={props.disabled}
readonly={props.disableInput}
// set keyboard in modern browsers
inputmode={props.integer ? 'numeric' : 'decimal'}
placeholder={props.placeholder}
aria-valuemax={props.max}
aria-valuemin={props.min}
aria-valuenow={current.value}
onBlur={onBlur}
onInput={onInput}
onFocus={onFocus}
onMousedown={onMousedown}
/>
<button
v-show={props.showPlus}
type="button"
style={buttonStyle.value}
class={[
bem('plus', { disabled: plusDisabled.value }),
{ [HAPTICS_FEEDBACK]: !plusDisabled.value },
]}
aria-disabled={plusDisabled.value || undefined}
{...createListeners('plus')}
/>
</div>
);
原生标签 button 上的属性和事件
- v-show showMinus showPlus
- aria-disabled 是一种用于辅助技术的属性,它用于指示一个控件是否禁用或不可用。
- class 和 style 属性计算出来
- onClick
- onTouchstartPassive
- onTouchend
- onTouchcancel
- 事件通过 actionType 区分加减号复用到函数内
原生标签 input 上的属性和事件
- v-show showInput
- ref inputRef
- class
- value
- style
- disabled 控制整个组件不可操作
- readonly 控制自身 input 标签只读,可以通过按钮操作数据
- type {props.integer ? 'tel' : 'text'} 整数用 tel 类型 弹起数字键盘
- inputmode props.integer ? 'numeric' : 'decimal' numeric:表示输入法应该弹出数字键盘,适用于需要输入数字的控件;decimal:表示输入法应该弹出带小数点的数字键盘,适用于需要输入带有小数的数字的控件;这是 HTML5 中新增属性,优先级应该比 type 高,做到渐进增强
- placeholder
- role "spinbutton" role 属性是一种用于改变 HTML 元素的角色或语义的属性。它可以让开发者在 HTML 元素上指定一个 ARIA 角色(Accessible Rich Internet Applications 角色),从而为屏幕阅读器等辅助技术提供更多的信息,以提高网站的可访问性。spinbutton:表示输入框是一个微调框,用户可以通过上下箭头对值进行微调;
- aria-valuemax 属性是指定可调节属性的最大值的 ARIA 属性(Accessible Rich Internet Applications 属性)。它用于指定控件的最大值,以便屏幕阅读器等辅助技术可以读取和报告控件的值范围。
- aria-valuemin 属性是指定可调节属性的最小值的 ARIA 属性(Accessible Rich Internet Applications 属性)。它用于指定控件的最小值,以便屏幕阅读器等辅助技术可以读取和报告控件的值范围。
- aria-valuenow 属性是指定可调节属性的当前值的 ARIA 属性(Accessible Rich Internet Applications 属性)。它用于指定控件的当前值,以便屏幕阅读器等辅助技术可以读取和报告控件的当前值。
- onBlur
- onInput
- onFocus
- onMousedown
组件数据流实现逻辑
// 比对值,忽略类型差别
const isEqual = (value1?: Numeric, value2?: Numeric) =>
String(value1) === String(value2);
// 格式化数据格式
const format = (value: Numeric, autoFixed = true) => {
const { min, max, allowEmpty, decimalLength } = props;
// 兼容空值展示
if (allowEmpty && value === '') {
return value;
}
value = formatNumber(String(value), !props.integer);
// 兼容空值转0
value = value === '' ? 0 : value;
// 转数字类型
value = Number.isNaN(value) ? min : value;
// whether to format the value entered by the user
// 自动修正初始值再范围内
value = autoFixed ? Math.max(Math.min( max, value), min) : value;
// format decimal
// 小数点精度
if (isDef(decimalLength)) {
value = value.toFixed( decimalLength);
}
return value;
};
// 初始值函数 主要格式化组件显示值 返回格式化后的值做响应式
// update:modelValue 单向修改父组件数据
// watch props.modelValue 再修改 current的值
const getInitialValue = () => {
const defaultValue = props.modelValue ?? props.defaultValue;
const value = format(defaultValue);
if (!isEqual(value, props.modelValue)) {
emit('update:modelValue', value);
}
return value;
};
const current = ref(getInitialValue());
watch(
() => props.modelValue,
(value) => {
if (!isEqual(value, current.value)) {
current.value = format(value!);
}
}
);
watch(current, (value) => {
emit('update:modelValue', value);
emit('change', value, { name: props.name });
});
数据处理封装
// beforeChange钩子做拦截器控制current值变化
const setValue = (value: Numeric) => {
if (props.beforeChange) {
callInterceptor(props.beforeChange, {
args: [value],
done() {
current.value = value;
},
});
} else {
current.value = value;
}
};
// 支持同步、异步自定义拦截函数
export function callInterceptor(
interceptor: Interceptor | undefined,
{
args = [],
done,
canceled,
}: {
args?: unknown[];
done: () => void;
canceled?: () => void;
}
) {
if (interceptor) {
// eslint-disable-next-line prefer-spread
const returnVal = interceptor.apply(null, args);
if (isPromise(returnVal)) {
returnVal
.then((value) => {
if (value) {
done();
} else if (canceled) {
canceled();
}
})
.catch(noop);
} else if (returnVal) {
done();
} else if (canceled) {
canceled();
}
} else {
done();
}
}
// 通过actionType判断是加还是减,然后调用setValue修改值
// 触发plus或minus事件或超出限制触发overlimit事件
const onChange = () => {
if (
(actionType === 'plus' && plusDisabled.value) ||
(actionType === 'minus' && minusDisabled.value)
) {
emit('overlimit', actionType);
return;
}
const diff = actionType === 'minus' ? -props.step : props.step;
const value = format(addNumber( current.value, diff));
setValue(value);
emit(actionType);
};
// 手动变更数据
// 设置 integer 属性后,输入框将限制只能输入整数。
// decimal-length 固定显示的小数位数
const onInput = (event: Event) => {
const input = event.target as HTMLInputElement;
const { value } = input;
const { decimalLength } = props;
let formatted = formatNumber(String(value), !props.integer);
// limit max decimal length
if (isDef(decimalLength) && formatted.includes('.')) {
const pair = formatted.split('.');
formatted = `${pair[0]}.${pair[1].slice(0, decimalLength)}`;
}
if (props.beforeChange) {
input.value = String(current.value);
} else if (!isEqual(value, formatted)) {
input.value = formatted;
}
// prefer number type
const isNumeric = formatted === String( formatted);
setValue(isNumeric ? formatted : formatted);
};
touch
函数 支持长按触发效果
不熟识移动端 click
和 touch
的事件逻辑,单从 createListeners
函数看会以为触发多次,以下:
在移动设备上,用户与屏幕的交互主要包括触摸和点击两种方式。当用户触摸或点击屏幕时,浏览器会触发一系列事件,包括 touchstart、touchmove、touchend、touchcancel、click 等事件。这些事件的触发顺序和触发逻辑如下:
-
touchstart:当用户触摸屏幕时触发,表示一个或多个手指已经放在了屏幕上。在此事件中,可以获取触摸点的坐标、时间戳等信息。
-
touchmove:当用户在屏幕上滑动手指时触发,表示触摸点的位置发生了变化。在此事件中,可以获取滑动过程中触摸点的坐标、时间戳等信息。
-
touchend:当用户从屏幕上抬起手指时触发,表示触摸事件结束。在此事件中,可以获取触摸结束时的坐标、时间戳等信息。
-
touchcancel:当触摸事件被中断时触发,例如系统中断了触摸事件、页面被隐藏等。
-
click:当用户在屏幕上点击元素时触发,表示用户已经完成了一个简单的点击操作。在此事件中,可以获取鼠标点击的位置、时间戳等信息。
-
在移动设备上,触摸事件和点击事件是互斥的,即当触摸事件被触发时,浏览器不会立即触发点击事件,而是等待一定时间,如果用户没有继续滑动手指,则会触发点击事件。这个时间间隔通常为 300 毫秒,称为“点击延迟”(click delay)。
const createListeners = (type: typeof actionType) => ({
onClick: (event: MouseEvent) => {
// disable double tap scrolling on mobile safari
// 在移动设备上,当用户双击屏幕时,浏览器会默认缩放页面。
// 例如在 Stepper 组件中,缩放页面可能会导致一些问题,例如界面变形或者用户误操作等。
preventDefault(event);
actionType = type;
onChange();
},
onTouchstartPassive: () => {
actionType = type;
onTouchStart();
},
onTouchend: onTouchEnd,
onTouchcancel: onTouchEnd,
});
//onTouchstartPassive onTouchStart 是 JavaScript 中的事件处理程序,当用户触摸设备屏幕时触发该事件。默认情况下,当 onTouchStart 事件被触发时,浏览器会等待事件处理程序完成后再滚动页面。这可能会导致用户界面响应时间延迟,特别是在移动设备上更为明显。
const onTouchStart = () => {
if (props.longPress) {
isLongPress = false;
clearTimeout(longPressTimer);
longPressTimer = setTimeout(() => {
isLongPress = true;
onChange();
longPressStep();
}, LONG_PRESS_START_TIME);
}
};
const onTouchEnd = (event: TouchEvent) => {
if (props.longPress) {
clearTimeout(longPressTimer);
if (isLongPress) {
preventDefault(event);
// 由于在移动设备上,长按按钮会触发默认的长按菜单(类似于右键菜单),
// 从而干扰了 Stepper 组件的长按功能。
// 为了避免这种情况发生,需要在 touchstart 事件中调用 event.preventDefault() 方法来阻止默认的长按菜单的弹出。
}
}
};
useCustomFieldValue
hook 实现跨组件间数据传输,实现表单校验、重置等效果
vue2
的时候多是是通过原型链往上级 parent 找最近的 form 以及 formitem 节点再触发组件内事件来传递事件。反之,上级要递归子节点找到后代目标节点传递事件
- stepper 组件通过
useCustomFieldValue
inject 注入 Field 组件的一些事件以及值,然后 watch modelValue 变化触发 Field 组件事件以及更新数值 - Field 组件把内部事件暴露到 Field 组件 instance 上,通过 ref 可以获取到 Field 实例并调用实例方法
- Form 组件把事件暴露到 instance 上, 然后我们就可以直接调用,例如调用
validate
事件做表单校验,validate
函数会通过 useChildren 寻找所有 Field 组件,调用其身上对应暴露的 validate 事件校验 stepper 组件的值是否满足校验规则要求
// stepper组件
useCustomFieldValue(() => props.modelValue);
// useCustomFieldValue函数实现
export function useCustomFieldValue(customValue: () => unknown) {
const CUSTOM_FIELD_INJECTION_KEY: InjectionKey<CustomFieldInjectionValue> =
Symbol('van-field');
const field = inject(CUSTOM_FIELD_INJECTION_KEY, null);
if (field && !field.customValue.value) {
field.customValue.value = customValue;
// 监听值变化触发field上面的事件
watch(customValue, () => {
field.resetValidation();
field.validateWithTrigger('onChange');
});
}
}
// Field组件
provide(CUSTOM_FIELD_INJECTION_KEY, {
customValue, // 当前值
resetValidation, // 重置校验
validateWithTrigger, // 校验当前值
});
// 把事件暴露到Field组件instance上,通过 ref 可以获取到 Field 实例并调用实例方法
useExpose <
FieldExpose >
{
blur,
focus,
validate,
formValue,
resetValidation,
getValidationStatus,
};
// Form组件
useExpose({
submit,
validate,
getValues,
scrollToField,
resetValidation,
getValidationStatus,
});
// useExpose 函数实现
// 把事件暴露到instance上,就可以直接调用组件内部的方法
export function useExpose<T = Record<string, any>>(apis: T) {
const instance = getCurrentInstance();
if (instance) {
extend(instance.proxy as object, apis);
}
}
学习 Vant 总结
bem
的封装 classname- 标签上大量辅助技术的属性
preventDefault
阻止原生事件- 结合
useCustomFieldValue
useChildren
等 hook 实现跨组件数据以及事件传输 - 最近用到了 Vant,看着简单,写篇文章好难
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhgaigai
系列文章
更多
同类精品
更多
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13