一、组件的使用
使用 cra 搭建一个 新的 react 项目, 当前 react 版本是 18.2.0,在 app.js 中使用 slider
function App() {
const [inputValue, setInputValue] = useState(19);
const onChange = (newValue) => {
setInputValue(newValue);
};
return (
<div className="App">
<h2>我是slider</h2>
<div style={{ width: "60%" }}>
<div>value 值--- {inputValue}</div>
<Slider
min={1}
max={100}
defaultValue={20}
onChange={onChange}
value={typeof inputValue === "number" ? inputValue : 0}
/>
</div>
</header>
</div>
);
}
export default App;
以上就可以直接使用单向数据流在 slider 上显示当前 value 值,也可以使用 slider 来改变 value 值;
二、源码概览
1、slider的属性, ref ,事件,方法
属性
// direction 直接使用 ltr
// vertical 直接 fase
// step 1
// min 0
// max 100
// reverse 直接 false
// dragging onStartDrag
// draggingIndex
// draggingValue
// cacheValues
// keyboardValue setKeyboardValue
// mergedValue rawValues setValue
// formatValue offsetValues
// mergedMin mergedMax
ref
containerRef
slider 组件的外层盒子,即最外层轨道;
事件
onSliderMouseDown
当鼠标点击时,计算出发点的水平位置,此处只考虑 direaction 为左到右,getBoundingClientRect 用法可以参考 MDN 文档;此时只会改变 values 里的第一个值,其他的不考虑;此处点击得到值与 滑块的拖动无关,当修改 value 值后会触发第一个滑块的位置同步更新;
const onSliderMouseDown = (e) => {
e.preventDefault();
const {
width, // 包含,border 和 padding
height,
left,
top,
bottom,
right,
} = containerRef.current.getBoundingClientRect();
const { clientX, clientY } = e;
let percent = (clientX - left) / width; // 此处只考虑 direction='ltr'
const nextValue = mergedMin + percent * (mergedMax - mergedMin);
const newValue = formatValue(nextValue); // 按照 step 处理数值
changeToCloseValue(newValue);
};
方法
getTriggerValue
源码如下,有 range 时就返回全部的 values , 没有 range 时,values 只有一个值,返回 values[0] 即可;triggerValues 既是 values 的浅复制, [...values] ;
const getTriggerValue = (triggerValues) => range ? triggerValues : triggerValues[0];
triggerChange
拿到新值后干点什么,onChange 在此时执行;
const triggerChange = (nextValues) => {
const cloneNextValues = [...nextValues].sort((a, b) => a - b);
if (onChange && !shallowEqual(cloneNextValues, rawValuesRef.current)) {
onChange(getTriggerValue(cloneNextValues));
}
setValue(cloneNextValues);
};
changeToCloseValue
可以在这里面执行 onBeforeChange 和 onAfterChange;
const changeToCloseValue = (newValue) => {
if (!disabled) {
let valueIndex = 0;
const cloneNextValues = [...rawValues];
cloneNextValues[valueIndex] = newValue;
// onBeforeChange?.(getTriggerValue(cloneNextValues));
triggerChange(cloneNextValues);
// onAfterChange?.(getTriggerValue(cloneNextValues));
}
};
2、三个 hook
1 useDrag
const [draggingIndex, draggingValue, cacheValues, onStartDrag] = useDrag(
containerRef,
direction,
rawValues,
mergedMin,
mergedMax,
formatValue, // 真实 value 需要被格式化,根据 min, max, step
triggerChange, // 函数,接收值的更新
finishChange, // mousemove 之后需要执行的回调
offsetValues // useOffset 返回的
);
2 useOffset
formatValue 是一个函数,将每次得到额值格式化,这里是根据 mix, max ,step 三个值来进行计算。offsetValues 是一个函数,
const [formatValue, offsetValues] = useOffset(
mergedMin,
mergedMax,
mergedStep,
);
// 根据 range 和 step 来格式化
const formatRangeValue = React.useCallback(
(val) => {
let formatNextValue = isFinite(val) ? val : min;
formatNextValue = Math.min(max, val);
formatNextValue = Math.max(min, formatNextValue);
return formatNextValue;
},
[min, max]
);
const formatStepValue = React.useCallback(
(val) => {
if (step !== null) {
const stepValue =
min + Math.round((formatRangeValue(val) - min) / step) * step;
const getDecimal = (num) => (String(num).split(".")[1] || "").length;
const maxDecimal = Math.max(
getDecimal(step),
getDecimal(max),
getDecimal(min)
);
const fixedValue = Number(stepValue.toFixed(maxDecimal));
return min <= fixedValue && fixedValue <= max ? fixedValue : null;
}
return null;
},
[step, min, max, formatRangeValue]
);
// 根据 mix,max,step
const formatValue = React.useCallback(
(val) => {
// ...
return formatStepValue(val) // 简化后的计算
},
[min, max, step]
);
// offset 是 valueIndex 所在滑块的 move 偏移量,offsetValues 方法只在 move 时调用,得到某个滑块的新值,以及当前所有滑块的经过 formatValue 处理后的新值;
const offsetValues = (values, offset, valueIndex, mode = "unit") => {
//...
return {
value: nextValues[valueIndex],
values: nextValues,
};
}
3 useMergedState
主要将 value 值和 defaultValue 值进行合并计算, 优先级: value---> 0,最后 mergedValue 的初始值为 value 或 0, 此时 min =1,max=100
import useMergedState from "rc-util/lib/hooks/useMergedState";
const [mergedValue, setValue] = useMergedState(defaultValue, {
value,
});
3、两个第三方库
1 classNames
合并 class
<div
ref={containerRef}
className={classNames(prefixCls, className, {
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-vertical`]: vertical,
[`${prefixCls}-horizontal`]: !vertical,
})}
style={style}
onMouseDown={onSliderMouseDown}
>
2 shallowEqual
顾名思义,浅层判断两个值是否相等;
shallowEqual(cloneNextValues, rawValuesRef.current);
4、子组件
一共有四个,Handels, Tracks,Steps,Marks,这里只考虑最常用最简单的场景,只需要 Handels
1 Handles
根据 value 的数量来生成 handel ;handel 就是一个绝对定位的空 div 滑块;
{values.map((value, index) => (
<Handle
dragging={draggingIndex === index}
prefixCls={prefixCls}
style={getIndex(style, index)}
key={index}
value={value}
valueIndex={index}
onStartMove={onStartMove}
render={handleRender}
{...restProps}
/>
))}
-
滑块组件,根据 传入的 value 值,mix, max,三个属性来计算绝对定位的值,从而决定滑块位置,当滑块被拖动时,会触发修改 vlaue ,从而更新就绝对定位位置;当value 是数组时,会生成多个滑块;
-
values 数组的 index 会从 map 函数传入 handel, 当 handel 被拖动时,index 会从 useDrag 提供的 onStartMove 函数 传入,修改 useDrag 内的 draggingIndex,draggingIndex 再出来进入 slider ,然后从handles 进入每个 handel,最后用来判断 每个 handel 的 dragging 属性是否为 true。也就是说,draggingIndex 的作用只有一个,就是和每个 handel 的 index 对比来判断各个 handel 的 dragging 属性为 true 还是 false。 这里兜转了一圈,相当辛苦,只因为单向数据流。
-
其中发现了一个可以用键盘操作的库
import KeyCode from 'rc-util/lib/KeyCode';
三、实现雏形,点击得到值;
修改 value 有两种方式,在轨道上点击 鼠标,拖动滑块;
思路:当在轨道上点击鼠标时,获取mouseDown 事件的 clientX,clientY(值与页面滚动无关,只与浏览器有关),然后根据 轨道的 width,min,max,计算出 value 值,这就是点击轨道得到值。与后面的滑块拖拽无关,但是值改变了可以影响滑块的位置;
注意:点击轨道只会修改 vlaues 中第一个的值,对其他的 value 无影响;
const onSliderMouseDown = (e) => {
e.preventDefault();
const {
width,
height,
left,
top,
bottom,
right,
} = containerRef.current.getBoundingClientRect();
const { clientX, clientY } = e;
let percent = (clientX - left) / width;
const nextValue = mergedMin + percent * (mergedMax - mergedMin);
const newValue = formatValue(nextValue); // 按照 step 格式化处理数值
console.log("newValue-----", newValue);
console.log("nextValue-----", nextValue);
changeToCloseValue(newValue);
};
四、滑块的静态值和动态值;
-
滑块其实就是一个 空的 div 盒子,当禁止时,使用绝对定位来巨顶其在 轨道槽上的位置,拖动时,会触发 onMove 来修改 value 值,然后单向数据 value 值 会影响其绝对定位得位置,实现 UI 与状态的 统一;
-
滑块还有一个 active 和 hover 的 css 属性,改变鼠标的形式;
.rc-slider-handle:hover { border-color: #57c5f7; } .rc-slider-handle:active { border-color: #57c5f7; box-shadow: 0 0 5px #57c5f7; cursor: -webkit-grabbing; cursor: grabbing; } .rc-slider-handle-dragging.rc-slider-handle-dragging.rc-slider-handle-dragging { border-color: #57c5f7; box-shadow: 0 0 0 5px #96dbfa; }
五、滑块的拖动;
useDrag 中返回一个 onStartDrag ,当滑块上触发 onMouseDown 时,就开始执行 onInternalStartMove ;valueIndex 是保证 values有多个时,多个滑块动谁就改变谁,不动的不改变;
// handel.js
// onStartMove 和 valueIndex 从 slider 中传入,其中 onStartMove 从useDrag中来
const onInternalStartMove = e => {
if (!disabled) {
onStartMove(e, valueIndex);
}
};
// useDrag.js
// 源码中考虑了移动端
function getPosition(e) {
const obj = "touches" in e ? e.touches[0] : e;
return {
pageX: obj.pageX,
pageY: obj.pageY,
};
}
const onStartMove = (e, valueIndex) => {
e.stopPropagation();
const originValue = rawValues[valueIndex];
setDraggingIndex(valueIndex);
setDraggingValue(originValue);
setOriginValues(rawValues);
// pageX 和 pageY 包含需要考虑滚动
const { pageX: startX, pageY: startY } = getPosition(e);
const onMouseMove = (event) => {
event.preventDefault();
const { pageX: moveX, pageY: moveY } = getPosition(event);
const offsetX = moveX - startX;
const offsetY = moveY - startY;
const { width, height } = containerRef.current.getBoundingClientRect();
let offSetPercent;
if (direction === 'ltr') offSetPercent = offsetX / width
updateCacheValueRef.current(valueIndex, offSetPercent);
};
const onMouseUp = (event) => {
event.preventDefault();
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("touchend", onMouseUp);
document.removeEventListener("touchmove", onMouseMove);
mouseMoveEventRef.current = null;
mouseUpEventRef.current = null;
setDraggingIndex(-1);
finishChange();
};
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("touchend", onMouseUp);
document.addEventListener("touchmove", onMouseMove);
mouseMoveEventRef.current = onMouseMove;
mouseUpEventRef.current = onMouseUp;
};
六、总结
官方组件好用的原因, 组件之间解耦清晰,各组件和 hook 职责分明,还有对象引用互不关联,经常有对象或数组的解构拷贝使用,最后就是各种 null 和 undefined 的判断,代码健壮性好。除去核心功能代码,健壮性兼容代码占比很重,保证了代码怎么玩都不会报错。
网友评论