内容简介:数字输入框,如下图,就是一个有着加减按钮的input而已,多用于购物车商品数目添加减少,这个输入框组件初看上去应该不是很难,但是Element的具体实现却有很多值得学习的地方,看完源码才感觉真难!官网代码这个组件的html结构较为简单,第一眼看上去我会以为是外层一个div,内层一个input,左右各一个span作为按钮,查看源码后也确实是这样,简化后的html结构如下前2个span是加和减的按钮,最后的
数字输入框,如下图,就是一个有着加减按钮的input而已,多用于购物车商品数目添加减少,这个输入框组件初看上去应该不是很难,但是Element的具体实现却有很多值得学习的地方,看完源码才感觉真难!官网代码 点此
数字输入框的html结构
这个组件的html结构较为简单,第一眼看上去我会以为是外层一个div,内层一个input,左右各一个span作为按钮,查看源码后也确实是这样,简化后的html结构如下
<div class='el-input-number'> <span class="el-input-number__decrease"></span> <span class="el-input-number__increase"></span> <el-input></el-input> </div> 复制代码
前2个span是加和减的按钮,最后的 <el-input>
是之前封装的输入框组件,注意不是原生的input,这里值得一提的是2个span都是绝对定位,且 <el-input>
的左右padding都是50px,如下图
也就是说这里的2个加减按钮是放在input的padding位置上的,是个包含关系而不是并排关系,2个span绝对定位,左边的left:1,右边的right:1,这种实现方式的好处我觉得是这样,如下图
当输入框获得焦点,输入框的border会高亮,给人一种这3部分是一个整体的感觉,css处理起来很简单,如果是3个部分并排,则还要单独处理左右2个span的border
具体各部分分析
先来看外层的div
<div @dragstart.prevent :class="[ 'el-input-number', inputNumberSize ? 'el-input-number--' + inputNumberSize : '', { 'is-disabled': inputNumberDisabled }, { 'is-without-controls': !controls }, { 'is-controls-right': controlsAtRight } ]"> 复制代码
第一行 @dragstart.prevent
第一眼看到这个我是懵逼的!这句话表明禁止了div的默认拖动行为,这里不是很明白,首先如果div要被拖动的话得设置 draggable="ture"
才行,而且为啥要禁止拖动?我试了下去掉这句话,再拖动这个组件
发现当你选中input中的数字时可以拖动数字出去,上图下面的浅色数字就是拖动出去的样子,还有个鼠标禁止的图案没能够截图,加上 draggable="ture"
就不能拖动选中的数字了
'el-input-number'
规定了外层div的基本类,如下
可见div被设置为inline-block内联元素,然后设置了宽度,因为组件宽度是不会随着内容的变化而变化,所以定死了宽度,接下来3个类分别控制组件是否禁用(禁用逻辑前面几篇已经分析过了),是否显示加减按钮,是否将按钮放置于右侧,见下图
这3个类是否添加都是由用户传入对应的prop来实现,上图一个令我搞了好半天的scss代码就是,上图中右下角的减按钮
@include when(controls-right) { @include e(decrease) { right: 1px; bottom: 1px; top: auto; left: auto; border-right: none; border-left: $--border-base; border-radius: 0 0 $--border-radius-base 0; } } 复制代码
这里的意思是当 controls-right
类被加上后, decrease
这个类的css变化为上面的内容,也就是将减按钮从原本的左侧放置到右下角,我开始不明白这里的 top:auto,left:auto
是干嘛的,后来控制台调试得知,因为decrease类原本的top是1px,left是1px,当 controls-right
类被加上后,必须得设置top,left为auto,让浏览器自动计算top和left,否则就无法覆盖原本的top:1px,left:1px。另外一个值得一提的是,这里的加减按钮的height是如下设定的
height: auto; line-height: #{($--input-height - 2) / 2}; 复制代码
指定height为auto,通过设置line-height值为输入框高度的一半减去border宽度来撑开高度,如果直接设置height为高度的一半也应该可以吧??然后输入框内的文字居中就是 text-align:center
实现
接下来看关键的加减按钮的逻辑实现,html代码如下,这个按钮是span实现的,不是原生button
<span class="el-input-number__decrease" role="button" v-if="controls" v-repeat-click="decrease" :class="{'is-disabled': minDisabled}" @keydown.enter="decrease"> <i :class="`el-icon-${controlsAtRight ? 'arrow-down' : 'minus'}`"></i> </span> 复制代码
role属性作用是告诉Accessibility类应用(比如屏幕朗读程序,为盲人提供的访问网络的便利程序),这个元素所扮演的角色,主要是供残疾人使用。使用role可以增强文本的可读性和语义化,然后v-if的controls是个bool值,是用户传入的prop,用来控制是否显示该按钮,然后 :class
控制了该按钮是否显示禁用样式, @keydown.enter
又让我疑惑了,这是在监听enter键按下,Vue官网相关的说明是给input加上这个事件,在input获得焦点时按下enter会触发对应的事件,但是为啥要给span也加个 @keydown.enter
,我试了点击enter没有任何反应,总之这里没搞明白
然后发现没有这个按钮没有@click事件,所有的点击处理逻辑都放在了 v-repeat-click="decrease"
里面,这里除了单击操作会使数字增加减少外,还有鼠标一直按着不放会快速增加减少数字,所有的逻辑都通过Vue中的自定义指令(directives)来实现,自定义指令通常用来对底层dom元素进行操作,触发特定的逻辑。在 directives
属性里进行声明
directives: { repeatClick: RepeatClick } 复制代码
这个key(repeatClick)就对应 v-repeat-click
,value(RepeatClick)是import进来的方法,代码见下面
import { once, on } from 'element-ui/src/utils/dom'; export default { bind(el, binding, vnode) { let interval = null; let startTime; const handler = () => vnode.context[binding.expression].apply(); const clear = () => { if (new Date() - startTime < 100) { handler(); } clearInterval(interval); interval = null; }; on(el, 'mousedown', (e) => { if (e.button !== 0) return; startTime = new Date(); once(document, 'mouseup', clear); clearInterval(interval); interval = setInterval(handler, 100); }); } }; 复制代码
这段代码就稍微复杂点,首先要熟悉Vue的自定义指令的内容,自定义指令会提供几个钩子函数,用来在特定的时机触发特定的逻辑,见下图
这里使用了bind
钩子函数,可以理解为初始化调用一次,你想想这个指令内肯定是给元素绑定单击事件,所以只需要在bind内调用一次即可,然后
bind
的三个参数
el,binding,vnode
分别代表可操作的dom,一个binding对象,提供各种信息,和Vue编译生成的虚拟节点 binding对象如下
在bind
这个钩子函数内的逻辑需要触发让输入框内数字加减的方法,这个方法写在组件的
methods
内,那么如何得到这个方法呢,下面这句就能得到
const handler = () => vnode.context[binding.expression].apply(); 复制代码
这句话我只能说太高端,得去看源码才能写出来,首先vnode是vue生成的虚拟节点,就是一个js对象而已,里面属性很多,那么 context
又是啥,翻看vue源码得知vnode的结构如下
Component
类型的数据结构,这个Component是flow定义的结构,具体可看vue源码中的flow内的内容,Component就是组件,所以这个context就是该vnode所在的组件上下文,再来看
binding.expression
,官网说这就是
v-repeat-click="decrease"
中的decrease方法,这个方法写在组件的methods内,那么
context[binding.expression]
就是
context['decrease']
因此就拿到了组件内的decrease方法,类似于在组件中使用
this.decrease
一样,然后最后的
apply()
就很奇怪了,apply的用法是参数的第一个表示要执行的目标对象,如果为null或者undefined则表示在window上调用该方法,这里没有参数,那就是undefined,所以是在window上执行,这个我也不确定到底说的对不对,我把这句话改为
const handler = () => vnode.context[binding.expression].apply(vnode); 复制代码
也没出现错误,这里也没搞清楚为啥直接apply()就行,我再把上面的改成下面这种,也就是直接执行函数,也没报错,一切正常
const handler = () => vnode.context[binding.expression]() 复制代码
回到 bind
方法的逻辑,发现这里并没有任何的 click
出现,也就是说没有绑定单击鼠标的事件,这里因为要处理按下去连续触发decrease方法,所以把单击和连续按下都糅合到一起了,如下
on(el, 'mousedown', (e) => { if (e.button !== 0) return; startTime = new Date(); once(document, 'mouseup', clear); clearInterval(interval); interval = setInterval(handler, 100); }); 复制代码
on
这个方法来自于源码外层的目录,因为其他组件也能用到,所以抽离成一个公共方法放到util目录下。先看 on
的代码
export const on = (function() { if (!isServer && document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event && handler) { element.attachEvent('on' + event, handler); } }; } })(); 复制代码
这个方法就是给元素绑定事件,if-else处理了兼容性的情况, attachEvent
是ie的方法, addEventListener
是其他主流浏览器的方法。 on
的第三个参数就是事件处理函数, on
中第一句 if (e.button !== 0) return
的 e.button
是按下了鼠标的哪个键
onclick
只响应鼠标左键的按下,而
onmousedown
则响应3个键的按下,所以这里要区分。
on
最后一句 interval = setInterval(handler, 100)
设置了定时器定时执行handler方法从而每隔0.1s触发一次数字增加或减少事件,然后我们思考,按下去鼠标时给dom元素添加了事件:定时执行handler,那么在鼠标抬起时肯定要销毁这个定时器,否则将会无限触发handler方法,造成数字一直增加或减少,因此 once(document, 'mouseup', clear)
这句话就是在鼠标抬起时销毁定时器,先看clear方法
const clear = () => { if (new Date() - startTime < 100) { handler(); } clearInterval(interval); interval = null; }; 复制代码
里面就是clearInterval销毁定时器,前面的if逻辑很关键,在按下鼠标时记录一个时间,抬起鼠标时检测当前时间 - 按下时的时间 < 100毫秒,如果是则触发一次点击,如果不写这个if,则无法实现单击操作,因为如果不写,由于interval = setInterval(handler, 100),在按下后100毫秒后才会触发一次点击,则在100毫秒内抬起鼠标时interval已经被clear了。最后注意下 once(document, 'mouseup', clear)
, once
是只触发一次的高阶函数,代码如下
export const once = function(el, event, fn) { var listener = function() { if (fn) { fn.apply(this, arguments); } off(el, event, listener); }; on(el, event, listener); }; 复制代码
这就是观察者模式里面的once的写法,本质上是复用on事件,只不过on的第三个参数加了修改,listener里会执行fn一次,然后就用off方法移除listener,因此达到了只执行一次的目的。 还有个注意的点,once方法的第一个参数是document,这个也很关键,你可能以为在加减按钮上绑定onmousedown就应该在加减按钮上绑定onmouseup,这样做就会出bug,考虑一种情况,当你鼠标在加减按钮上按下时,然后移动鼠标到按钮外,再放开鼠标,此时会发现数字还在增加,这就是bug,因此要在document这个最外层的dom元素上绑定mouseup,这样mouseup事件总能被响应,否则乱移动鼠标就会造成数字一直增加
分析不动了,主要难点已经写完了,剩下的精度属性和step其实也不难,总之要搞懂所有的代码很难,只能关注部分核心逻辑
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Element源码分析系列5-Input(输入框)
- PHP 对输入变量名的自动转换的问题与源码分析
- iOS个人中心渐变动画、微信对话框、标签选择器、自定义导航栏、短信验证输入框等源码
- Android输入系统(一)输入事件传递流程和InputManagerService的诞生
- Android输入系统(四)输入事件是如何分发到目标窗口的?
- Android输入系统(二)IMS的启动过程和输入事件的处理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。