本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第38期,链接:经常用 vant-weapp 开发小程序,却不知道如何开发一个组件?学!。
这是我第一次写文章,以往也都是看了源码,然后...就没有然后了,在偶然的机会下,接触到了若川的这个计划,个人感觉蛮好,就试着参加了这个计划。
Taroify
Taroify 是移动端组件库 Vant 的 Taro React 版本,往常我经常使用 antd 的组件库,当前主要项目多用于 PC 端,这期的源码读取是 Stepper 组件。
结构
Stepper 的结构当前为 3 个部分:
- stepper.tsx 主要控制组件的输出和 Stepper 的主要逻辑
- stepper-button.tsx 就是作为 steppper 的按钮 => '+'/'-'
- stepper-input.tsx 是作为 用户输入的控制
主要是通过这 3 个部分仅限构建
通讯
Stepper 的数据源通信主要通过,Stepper 的 Props, 和 组件内部的 context 来通讯。 Stepper 的 Props 数据输入,这毫无问题,进行正常的数据(正常操作),主要部分是内部的 context 通讯。
<StepperContext.Provider
value={{
value: valueRef.current,
min,
max,
size,
disabled,
precision,
longPress,
formatValue,
onChange: setValue,
onStep,
}}
>
<View
className={classNames(
prefixClassname("stepper"),
{
[prefixClassname("stepper--square")]: shape === "square",
[prefixClassname("stepper--rounded")]: shape === "rounded",
[prefixClassname("stepper--circular")]: shape === "circular" || shape === "round",
},
className,
)}
{...restProps}
>
{decrease}
{input}
{increase}
</View>
</StepperContext.Provider>
StepperContext 来组织,组件之间的数据传递,这当前有个好处,组件之间所有的 context 都一样,也就保证了组件内的数据输入的统一性。
逻辑
Stepper 的值逻辑在于 Stepper.tsx 函数中
const onStep = useCallback(
(actionType: StepperActionType) => {
const diff = actionType === "decrease" ? -step : +step
setValue(formatValue(addNumber(valueRef.current as number, diff)))
},
[formatValue, setValue, step, valueRef],
)
这函数,保证了你点击按钮的时候,主逻辑由 stepper.tsx 掌控,而值的输入有 formatValue 控制,保证输入值的有效性
const formatValue = useCallback(
(value: string | number) => {
if (value === "") {
return value
}
value = formatNumber(String(value), precision > 0)
value = value === "" ? 0 : +value
value = Number.isNaN(value) ? +min : value
value = Math.max(Math.min(+max, value), +min)
// format decimal
if (precision > 0) {
value = value.toFixed(+precision)
}
return value
},
[max, min, precision],
)
而对于 stepper 组件内的小组件 children 的生成,主要由 useStepperChildren 输出
function useStepperChildren(children?: ReactNode): StepperChildren {
return useMemo(() => {
const __children__: StepperChildren = {}
Children.forEach(children, (child: ReactNode) => {
if (!isValidElement(child)) {
return
}
const element = child as ReactElement
const elementType = element.type
if (elementType === StepperButton) {
if (__children__.decrease === undefined) {
__children__.decrease = cloneElement(element, {
type: "decrease",
})
} else if (__children__.increase === undefined) {
__children__.increase = cloneElement(element, {
type: "increase",
})
}
} else if (elementType === StepperInput) {
__children__.input = element
}
})
if (!children) {
const element = <StepperButton />
if (__children__.decrease === undefined) {
__children__.decrease = cloneElement(element, { type: "decrease" })
}
if (__children__.input === undefined) {
__children__.input = <StepperInput />
}
if (__children__.increase === undefined) {
__children__.increase = cloneElement(element, { type: "increase" })
}
}
return __children__
}, [children])
}
这个函数保证了,在你 children 值没有传递时,能够正常的渲染出来,设置了预设。 这是当前主要的一个逻辑映射,下面是一些内部小组件的逻辑。
stepper-button
const longPressStep = useCallback(() => {
longPressTimerRef.current = setTimeout(() => {
onStep?.(type)
longPressStep()
}, LONG_PRESS_INTERVAL)
}, [onStep, type])
const handleTouchStart = useCallback(() => {
if (longPress) {
longPressRef.current = false
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current)
}
longPressTimerRef.current = setTimeout(() => {
longPressRef.current = true
onStep?.(type)
longPressStep()
}, LONG_PRESS_START_TIME)
}
}, [longPress, longPressStep, onStep, type])
const handleTouchEnd = useCallback(
(event: ITouchEvent) => {
if (longPress) {
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current)
}
if (longPressRef.current) {
preventDefault(event)
}
}
},
[longPress],
)
button 这部分呢,主要是通过 setTimeout 进行一个防抖操作,已防止用户多次频繁点击,而导致数值变动异常。
stepper-input
主要是通过 onInput 进行数值的更改操作,但只有在 onBlur 失焦时,才会更新父内容,而保证,减少兄弟组件的重复 render
const handleFocus = useCallback(
(event: BaseEventOrig<InputProps.inputForceEventDetail>) => {
// readonly not work in legacy mobile safari
if (disabledProp) {
rootRef.current?.blur()
} else {
onFocus?.(event)
}
},
[disabledProp, onFocus],
)
const onInput = useCallback(
({ detail }: BaseEventOrig<InputProps.inputEventDetail>) => {
const { value: inputValue } = detail
let formatted = formatNumber(String(inputValue), digit)
// limit max decimal length
if (precision > 0 && formatted.includes(".")) {
const pair = formatted.split(".")
formatted = `${pair[0]}.${pair[1].slice(0, precision)}`
}
// prefer number type
const isNumeric = formatted === String(+formatted)
setValue(isNumeric ? +formatted : formatted)
},
[digit, precision],
)
const onBlur = useCallback(
({ detail }: BaseEventOrig<InputProps.inputValueEventDetail>) => {
const { value: inputValue } = detail
const value = formatValue?.(inputValue)
setValue(value)
onChange?.(value)
},
[formatValue, onChange],
)
总结
在这次 Stepper 源码阅读中,我们可以看到,我们尽量把内容放在组件内部,减少其他兄弟级的组件进行无用的重复渲染,在有 button 的前提下,我们可以通过防抖来处理,以减少无用的渲染或数值的频繁更新。