前言
平時開發過程中,出於各種原因模擬原生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
本文是自己的一篇學習總結記錄,不過我感覺最有用的還是對自己的觸動。因為平時都習慣於第一種方式去實現功能,特別是在業務開發過程中,第一選擇肯定是自己常用的。還是在空閑時候才有心情去優化。
這時候才清晰的理解我們所謂的讀優秀開源作品源碼,學習的是什么,不要為了讀源碼而讀源碼,有目的有思維的讀才能學習更多。望諸君共勉,再次對參考文章表示感謝。