zoukankan      html  css  js  c++  java
  • 模拟select,隐藏下拉列表的几种实现

    前言

    平时开发过程中,出于各种原因模拟原生slect的要求并不算少见。
    在实现的过程中,点击其他区域隐藏下拉列表,又是一个必备的功能,
    最近在一次开发的过程中引发了点思考,做下总结。

    现象

    实际中的实现比较复杂,列表中还要增删改查等操作。这里就只放个最简单的demo。
    目的是点击select以外的其他区域,隐藏下拉列表。
    效果大概这个样子(简单粗暴纯演示用):

    首先这确实不难实现,上来像方法一一样撸袖子干就完了
    开始之前,先列下基本结构,待会好描述:
    外层一个warper,里面是Input,下面就是ul,li绑定点击事件。

                <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)
                }
                
    

    实现方式有下面这么几种:

    实现一:全局监听点击事件,判断是否为select区域的子元素。

    这是原本比较熟悉和一直在使用的方式:

    //组件挂载之后添加事件
    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>
    

    那么效果就如下图所示了:

    此外实现方式总感觉不够优雅,所以我们应该考虑其他实现方式。

    实现二:select元素的焦点事件

    可能一开始思维固话之后,就不太好转变,因为上面的方式是一直所熟悉的,一时想不到其他方法。
    这时候可以去跟别人交流一下(这里的交流包括但不限于老司机面谈,搜索某种实现思路,优秀开源框架)。
    得到了另一个方向:点击其他区域的时候,意味着当前区域失去了焦点,
    基于这一点可以从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操作并没有得到相应。

    既然是执行顺序的问题,那么我们可以有下面两种解决思路:

    2.1 事件执行顺序不变,修改回调事件执行时机

    既然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>
    

    这样可以满足我们的需求,此外还有另一种方式

    2.2 改变事件执行顺序,即使用触发时机在blur之前的事件来替换click,即mouseDown

    大致说下几个事件的执行顺序(毕竟我对这方面掌握的也不是很不足,所以后面也会专门总结下相关内容)。

    // 这里也顺便解释了下问题出现的原因
    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
    本文是自己的一篇学习总结记录,不过我感觉最有用的还是对自己的触动。因为平时都习惯于第一种方式去实现功能,特别是在业务开发过程中,第一选择肯定是自己常用的。还是在空闲时候才有心情去优化。
    这时候才清晰的理解我们所谓的读优秀开源作品源码,学习的是什么,不要为了读源码而读源码,有目的有思维的读才能学习更多。望诸君共勉,再次对参考文章表示感谢。

  • 相关阅读:
    今日总结
    今日总结
    团队绩效1
    本周总结
    团队冲刺阶段10
    团队冲刺阶段9
    团队冲刺阶段8
    promise手写自定义封装异步任务回调的执行
    Vue中this.$options.data()和this.$data知多少?
    手写Promise自定义封装 then 函数
  • 原文地址:https://www.cnblogs.com/pqjwyn/p/10549109.html
Copyright © 2011-2022 走看看