内容简介:数字输入框,如下图,就是一个有着加减按钮的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的启动过程和输入事件的处理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Ordering Disorder
Khoi Vinh / New Riders Press / 2010-12-03 / USD 29.99
The grid has long been an invaluable tool for creating order out of chaos for designers of all kinds—from city planners to architects to typesetters and graphic artists. In recent years, web designers......一起来看看 《Ordering Disorder》 这本书的介绍吧!