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