模擬select,隱藏下拉列表的幾種實現

前言

平時開發過程當中,出於各類緣由模擬原生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)
            }

實現方式有下面這麼幾種:瀏覽器

實現一:全局監聽點擊事件,判斷是否爲select區域的子元素。

這是本來比較熟悉和一直在使用的方式: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

缺點一:性能消耗

每次都溯源去判斷,性能消耗是個問題,特別是稍微複雜頁面,展現多個組件時。函數

缺點二:受其餘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 本文是本身的一篇學習總結記錄,不過我感受最有用的仍是對本身的觸動。由於平時都習慣於第一種方式去實現功能,特別是在業務開發過程當中,第一選擇確定是本身經常使用的。仍是在空閒時候纔有心情去優化。 這時候才清晰的理解咱們所謂的讀優秀開源做品源碼,學習的是什麼,不要爲了讀源碼而讀源碼,有目的有思惟的讀才能學習更多。望諸君共勉,再次對參考文章表示感謝。

相關文章
相關標籤/搜索