平时开发过程当中,出于各类缘由模拟原生slect的要求并不算少见。
在实现的过程当中,点击其余区域隐藏下拉列表,又是一个必备的功能,
最近在一次开发的过程当中引起了点思考,作下总结。git
实际中的实现比较复杂,列表中还要增删改查等操做。这里就只放个最简单的demo。
目的是点击select之外的其余区域,隐藏下拉列表。
效果大概这个样子(简单粗暴纯演示用):
github
首先这确实不难实现,上来像方法一同样撸袖子干就完了
开始以前,先列下基本结构,待会好描述:
外层一个warper,里面是Input,下面就是ul,li绑定点击事件。segmentfault
<div className="match-select-warper" name={`this.idName`}> <Input></Input> <ul className={`${showOption ? '' : 'hidden'}`}> <li onClick={this.clickHanler}>{问题1}</li> <li onClick={this.clickHanler}>{问题1}</li> </ul> </div> // 点击列表,提示并隐藏弹框 clickHanler(){ alert('1') this.changeShow(false) }
实现方式有下面这么几种:浏览器
这是本来比较熟悉和一直在使用的方式:bash
//组件挂载以后添加事件 componentDidMount(){ // 非匿名函数的目的在于移除时解除事件 this.clickTriggerHandler = ((idName) => { let id = idName; return (event) => { // 是否属于子元素 !isParent(id, event.target) && (this.changeShow(false)); } })(this.idName) document.addEventListener('click', this.clickTriggerHandler) } componentWillUnmount() { // 若绑定事件,则移除该事件 if(this.clickTriggerHandler){ document.removeEventListener('click', this.clickTriggerHandler) } }
至于如何判断事件元素的归属也比较常见:
判断当前元素的父元素是否为置顶元素,不知足则循环上溯祖先元素,直到document。框架
/** * 判断是否属于指定元素的子元素 * @param {*} id 指定元素的标识 * @param {*} dom 触发事件的dom */ const isParent=(id, dom)=>{ let tempNode = dom.parentNode; while (tempNode && tempNode !== document) { // 知足则返回true if (tempNode.getAttribute('name') == id) { return true; } else { // 不然继续获取祖先元素 tempNode = tempNode.parentNode; } } // 最终返回false return false; }
这样达到了咱们的目的,不过是有些缺点的。dom
每次都溯源去判断,性能消耗是个问题,特别是稍微复杂页面,展现多个组件时。函数
假若有元素阻止了冒泡,若是点到了这个元素,那么全局就监听不到该事件了。性能
<button onClick={(e) => { e.nativeEvent.stopImmediatePropagation(); alert('我就是来阻止冒泡的') }}>测试</button>
那么效果就以下图所示了:
学习
此外实现方式总感受不够优雅,因此咱们应该考虑其余实现方式。
可能一开始思惟固话以后,就不太好转变,由于上面的方式是一直所熟悉的,一时想不到其余方法。
这时候能够去跟别人交流一下(这里的交流包括但不限于老司机面谈,搜索某种实现思路,优秀开源框架)。
获得了另外一个方向:点击其余区域的时候,意味着当前区域失去了焦点,
基于这一点能够从input操做了。
<div className="match-select-warper" name={`${this.idName}`}> <Input onFocus={(e) => { // 聚焦或者失焦时,彻底能够操做 this.changeShow(true) }} onBlur={(e) => { this.changeShow(false) }} ></Input> <ul className={`${showOption ? '' : 'hidden'}`}> <li onClick={this.clickHanler}>{问题1}</li> <li onClick={this.clickHanler}>{问题1}</li> </ul> </div>
这样看起来很美好,可是点击列表的时候,直接关闭了,没有执行this.clickHanler回调。
由于下拉列表操做点击的时候,其实对于Input而言也是失去焦点。
因此先执行了input的onBlur,隐藏列表,state更新以后,
列表的click操做并无获得相应。
既然是执行顺序的问题,那么咱们能够有下面两种解决思路:
既然blur执行顺序在前,从新渲染后会影响后续执行,那么咱们将blur事件的回调延迟执行,即不当即去setState,那么li的click事件就会执行,而后再去隐藏列表。
至于如何延迟执行,显然就是咱们的万能setTimeout了:
<div className="match-select-warper" name={`${this.idName}`}> <Input onFocus={(e) => { // 聚焦或者失焦时,彻底能够操做 this.changeShow(true) }} onBlur={(e) => { // 延迟执行 blur的回调,先执行 setTimeout(this.changeShow.bind(this,false),200) }} ></Input> <ul className={`${showOption ? '' : 'hidden'}`}> <li onClick={this.clickHanler}>{问题1}</li> <li onClick={this.clickHanler}>{问题1}</li> </ul> </div>
这样能够知足咱们的需求,此外还有另外一种方式
大体说下几个事件的执行顺序(毕竟我对这方面掌握的也不是很不足,因此后面也会专门总结下相关内容)。
// 这里也顺便解释了下问题出现的缘由 mousedown->blur->mouseup->click
既然click触发时机晚于blur,那咱们换成mouseDown不就绕过去了。
<div className="match-select-warper" name={`${this.idName}`}> <Input onFocus={(e) => { // 聚焦或者失焦时,彻底能够操做 this.changeShow(true) }} onBlur={(e) => { // 延迟执行 blur的回调,先执行 setTimeout(this.changeShow.bind(this,false),200) }} ></Input> // 列表的选择回调在mousedown时执行 <ul className={`${showOption ? '' : 'hidden'}`}> <li onMouseDown={this.clickHanler}>{问题1}</li> <li onMouseDown={this.clickHanler}>{问题1}</li> </ul> </div>
效果同上,这里就不重复放图了。
若是咱们的目的是点击列表的时候,彻底不触发blur事件,能够在clickHanler回调里加上event.preventDefault(),这样就不会按照原来的顺序出发blur事件了。例如这里:
// 自己自行处理了列表显示,就不用调用blur事件了 clickHanler(event){ event.preventDefault() alert('1') this.changeShow(false) }
具体是否阻止默认事件,就看具体应用了,示例代码这里就没有阻止默认事件,
而是将列表的显示隐藏全交给焦点事件来处理。
// 只关注点击的逻辑,公共逻辑交给blur统一管理 clickHanler(){ alert('1') }
即点击其余区域时,点击的是背景mask,交给他来统一处理。
由于这样点击存在一个比较明显的问题,若是想要点击其余元素例如radio时,须要二次点击。
因此这里就不去折腾这种实现了。
浏览器点击屏幕事件触发顺序
eagle-ui
https://segmentfault.com/q/1010000004950602 本文是本身的一篇学习总结记录,不过我感受最有用的仍是对本身的触动。由于平时都习惯于第一种方式去实现功能,特别是在业务开发过程当中,第一选择确定是本身经常使用的。仍是在空闲时候才有心情去优化。 这时候才清晰的理解咱们所谓的读优秀开源做品源码,学习的是什么,不要为了读源码而读源码,有目的有思惟的读才能学习更多。望诸君共勉,再次对参考文章表示感谢。