[Element plus]源码学习---Input Number数字输入框
1、结构
首先我们先从结构入手:
在element plus里面就拿一个最普通的input Number框来讲
翻开源码查看结构,我们得知就是一个很标准的一个大盒子里面装着两个span,一个el-input
2、@ dragstart.prevent
在查看结构的时候,从大盒子开始看,bem规范我们撇开不看,就很明显看到@dragstart.prevent @dragstart.prevent:拖拽开始就阻止自身的默认事件(禁止数字拖拽 /禁止图片被拖拽 属于vue自带事件)
3、加减事件
这里我们发现element plus 这里是使用v-repeat-click去触发加减事件的,v-repeat-click是什么呢? v-repeat-click其实就是重复点击,用于函数防抖的
v-repeat-click会注册mousedown事件,当用户连续点击+时: 当用户鼠标左键一直按住不松手,只会触发一次触发mousedown的回调,但实际测量el-input-number发现,输入框中的数字会持续变大,原因就在于mousedown回调中加入了定时器,当鼠标松开,触发一次mouseup回调方法,取消该定时器;这也许是directive为什么叫repeat-click的缘故吧; 如果时间间隔大于100毫秒,那么mousedown的回调方法里的setInterval回调就会执行(及handler,本质上就是执行上图的decrease或increase方法); 如果时间间隔小于100毫秒,定时器就会取消; mousedown的回调方法(clear方法)每次执行时,都会通过once方法注册并执行一次mouseup回调; 在mouseup回调中,如果发现距离最近一次点击时间小于100ms,就会执行一次handler方法,并清除定时器;
import { isFunction } from '@element-plus/utils'
import type { ObjectDirective } from 'vue'
export const REPEAT_INTERVAL = 100 //时间间隔
export const REPEAT_DELAY = 600 // 延迟时间
// 重复单击选项
export interface RepeatClickOptions {
interval?: number
delay?: number
handler: (...args: unknown[]) => unknown
}
export const vRepeatClick: ObjectDirective<
HTMLElement,
RepeatClickOptions | RepeatClickOptions['handler']
> = {
beforeMount(el, binding) {
const value = binding.value
const { interval = REPEAT_INTERVAL, delay = REPEAT_DELAY } = isFunction(
value
)
? {}
: value
let intervalId: ReturnType<typeof setInterval> | undefined
let delayId: ReturnType<typeof setTimeout> | undefined
//获取表达式内容
const handler = () => (isFunction(value) ? value() : value.handler())
// clear 函数 --- 清理定时器
const clear = () => {
if (delayId) {
clearTimeout(delayId)
delayId = undefined
}
if (intervalId) {
clearInterval(intervalId)
intervalId = undefined
}
}
// 注册事件监听,绑定鼠标按下事件
el.addEventListener('mousedown', (evt: MouseEvent) => {
if (evt.button !== 0) return // evt.button表示一个数值代表鼠标按键
clear()
handler()
// 监听鼠标抬起事件,但只监听一次
document.addEventListener('mouseup', () => clear(), {
once: true,
})
delayId = setTimeout(() => {
intervalId = setInterval(() => {
handler()
}, interval)
}, delay)
})
},
}
以上代码,小弟我有两个问题想问问各位同学 Q1:为什么监听的主体是document而不是像上面使用el呢? Q2:为什么监听鼠标抬起事件需要用once,而不像上面用直接on呢?
A1:因为在点击过程中,鼠标可能会移除当前el那个容器外面再进行抬起动作,所以需要绑定document而不是el A2:因为使用on的话会一直监听,而el容器外点击鼠标也会调用到到clear()
补充一些上述提及‘evt.button’的鼠标按键数值(鼠标事件):
当初查阅资料的时候,其实还有3:浏览器后退;4:浏览器前进,但不常用这里一笔带过。
// '加号'方法
const increase = () => {
// 判断是否 为 只读状态、inputNumber是否禁用状态、是否达到最大禁用值(max属性计算)
if (props.readonly || inputNumberDisabled.value || maxDisabled.value) return
const value = props.modelValue || 0
const newVal = ensurePrecision(value)
setCurrentValue(newVal)
emit(INPUT_EVENT, data.currentValue)
}
// '减号'方法
const decrease = () => {
// 判断是否 为 只读状态、inputNumber是否禁用状态、是否达到最小禁用值(min属性计算)
if (props.readonly || inputNumberDisabled.value || minDisabled.value) return
// modelValue 为v-model绑定的变量
const value = props.modelValue || 0
// ensurePrecision() 是通过将值转换为整数,解决JS的精度问题的一个函数来的
const newVal = ensurePrecision(value, -1)
// 设置当前值
setCurrentValue(newVal)
emit(INPUT_EVENT, data.currentValue)
}
是否达到最大禁用值(max属性计算)
4、精度
一个前端常识判断题:0.1+0.2 = 0.3 是否正确? 答案是错误的
如果这样的话,我们0.1+0.2这样的精度问题出现了。 那在Input Number里面是如何解决精度问题(将小数转换成整数计算,再除以位数)呢? 之前在学习的时候我看到别人文章中是这样写到:element的解决思路是将值扩大精度倍进行计算,得到结果后再除以精度倍数。 那我们来看一下源码
const increase = () => {
// 判断是否只读/禁用状态的值/最大限制的值
if (props.readonly || inputNumberDisabled.value || maxDisabled.value) return
// 拿到value 然后赋值给newVal 然后调用ensurePrecision()
const value = props.modelValue || 0
const newVal = ensurePrecision(value)
// 以下只作数值更新
setCurrentValue(newVal)
emit(INPUT_EVENT, data.currentValue)
}
const ensurePrecision = (val: number, coefficient: 1 | -1 = 1) => {
if (!isNumber(val)) return data.currentValue
// 通过将值转换为整数来解决JS小数计算的准确性问题
return toPrecision(val + props.step * coefficient)
}
// 对数值进行加工 确保误差情况会被消除
const toPrecision = (num: number, pre?: number) => {
if (isUndefined(pre)) pre = numPrecision.value
if (pre === 0) return Math.round(num)
let snum = String(num)
const pointPos = snum.indexOf('.')
if (pointPos === -1) return num
const nums = snum.replace('.', '').split('')
const datum = nums[pointPos + pre]
if (!datum) return num
const length = snum.length
if (snum.charAt(length - 1) === '5') {
snum = `${snum.slice(0, Math.max(0, length - 1))}6`
}
//最后 再调用toFixed()函数展示精度
return Number.parseFloat(Number(snum).toFixed(pre))
}
5、严格步进
在element Plus讲述过了step-strictly的使用方法
我们再去看看API,这里提示是否输入的step的倍数
// currentValue :缓存上次输入的值
// userInput :缓存当前输入框的值
//监听器 监听model绑定的value 进行更新
watch(
() => props.modelValue,
(value) => {
// 调用verifyValue()方法
data.currentValue = verifyValue(value, true)
data.userInput = null
},
{ immediate: true }
)
const verifyValue = (
value: number | string | null | undefined,
update?: boolean
): number | null | undefined => {
// 在这里把stepStrictly拿出来
const { max, min, step, precision, stepStrictly, valueOnClear } = props
let newVal = Number(value)
if (isNil(value) || Number.isNaN(newVal)) {
return null
}
if (value === '') {
if (valueOnClear === null) {
return null
}
newVal = isString(valueOnClear) ? { min, max }[valueOnClear] : valueOnClear
}
// 判断是否有这个标识
if (stepStrictly) {
newVal = toPrecision(Math.round(newVal / step) * step, precision)
}
if (!isUndefined(precision)) {
newVal = toPrecision(newVal, precision)
}
if (newVal > max || newVal < min) {
newVal = newVal > max ? max : min
update && emit(UPDATE_MODEL_EVENT, newVal)
}
return newVal
}
转载自:https://juejin.cn/post/7259926933008891941