日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

从 Dropdown 的 React 实现中学习到的

發布時間:2025/5/22 编程问答 21 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从 Dropdown 的 React 实现中学习到的 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Demo

Demo Link

Note

dropdown 是一種很常見的 component,一般有兩種:

  • 展開 dropdown menu 后,點擊任意地方都應該收起 menu。
  • 展開 dropdown menu 后,點擊 menu 內部,不會收起 menu,只有點擊 menu 外部,才收起 menu。
  • 在 jQuery 時代,dropdown 是很好實現的,直接用 document.addEventListener('click', handler),監聽 document 的 click 事件,然后讓 dropdown 的 menu 隱藏起來。如果想讓 menu 內部的點擊不收起 menu,則讓 menu 內部的點擊事件執行 event.stopPropagation()。

    剛開始做 React 開發的時候,不知道是從哪接收到的思想,覺得 document.addEventListener() 的 API 不那么 React,很排斥使用。這樣,在實現 dropdown component 時,怎么處理在 menu 以外點擊時讓 menu 收起來成了一個頭疼的問題。

    我查了文檔,覺得可以用 onBlur 這個事件,但為了能夠接收到 onBlur 事件,menu 內部必須是 input 類型的 component,或者是有 tabIndex 屬性,然后加上 tabIndex 后,當 component 處于 onFocus 時,會額外在邊框上加上陰影的樣式,像下圖所示,必須額外再加 css 處理。總之,邏輯變得復雜了。

    后來用 React 做音樂播放器,看別人的實現源碼,發現他們都大都使用了 audioElement.addEventListener('play', handler) 這種原生 API,而且,有些邏輯如果不用原生事件就沒法處理,比如監聽 window 的 resize 事件,似乎除了用 window.addEventListener('resize', handler) 就沒有其它辦法了。因此再回過頭來看 dropdown 的實現,如果也用 document.addEventListener('click', handler) 處理 menu 以后的點擊的話,邏輯就簡單多了。

    但是,也還是有坑的。

    坑之一,React 的 event.stopPropagation() 無法阻止原生事件冒泡到 document。

    看這篇文章的詳細介紹:

    • React 合成事件和原生事件的阻止冒泡

    React 的 issue:

    • e.stopPropagation() seems to not be working as expect.

    React 有兩套事件系統,一套是原生事件系統,就是 document.addEventListener() 這種 API,另一套是 React 自己定義的,叫 SyntheticEvent (合成事件),比如下例中的 onClick。

    <a onClick={this.clickLink}>Open</a> 復制代碼

    實際 React 的所有合成事件都是綁定在 document 上的 (所謂的代理方式),而不是單獨綁在各個 component 上,當你執行合成事件中的 event.stopPropagation() 時,實際原生事件已經到達 document 了。

    所以 React 的 event.stopPropagation() 只能阻止合成事件繼續往上冒泡,卻不能阻止原生事件往上冒泡到 document。

    所以你會發現,為什么我已經在 menu 內部的點擊事件 handler 中 stopPropagation 了,為什么全局的 click handler 還是會執行,這就是原因。

    但是! React 的合成事件的 stopPropagation 雖然不能阻止事件冒泡到 document,但它可以阻止事件冒泡到 window。

    (這件事讓我想起,在某個項目中,我用了 React 的 event.stopPropagation(),導致 turbolinks 不工作了,當時覺得很理所當然,現在回想,不對,turoblinks 綁定的是原生事件,如果它是綁在 <a> tag 上的話,不應該不工作的啊,由此我推斷 turbolinks 的 click 事件是綁定在 window 上的,后來看了源碼,的確是這樣的)

    所以,為了在 React 的 dropdown 中實現點擊 menu 外部收起 menu,點擊內部不收起 menu,有兩種辦法:

  • 使用 window.addEventLister('click', handler) 替代 document.addEventListener('click', handler),同時在 menu 內部點擊時,調用合成事件的 event.stopPropagation()

  • 不調用 event.stopPropagation(),讓事件冒泡到 document 的 click handler 中,在 handler 中判斷 event.target 中在 menu 內部還是外部,使用 DOMNode.contains() 方法判斷。這種方法需要用 React 的 ref 屬性把 menu 的引用保存下來,如下所示:

    <div className="dropdown-body" ref={ref=>this._dropdown_body=ref}> 復制代碼

    判斷:

    handleGlobalClick = (event) => {console.log('global click')// use DOMNode.contains() method to judge click target is in or out of the dropdown bodyif (this._dropdown_body && this._dropdown_body.contains(event.target)) returnthis.setState({dropDownExpanded: false})document.removeEventListener('click', this.handleGlobalClick)} 復制代碼
  • 坑之二,在原生事件的 handler 中,this.setState() 是同步的,不是異步的,讓我很驚訝。之前一直以為 this.setState() 肯定是異步的。

    具體的分析可以看這篇文章 - 你真的理解 setState 嗎?

    總結:

    setState 只在合成事件和生命周期函數中是 "異步" 的,在原生事件和 setTimeout 中都是同步的。

    但在 twitter 上看 Dan 發推說以后可能會統一成異步操作,拭目以待。

    其它細節:

  • 只有在 menu 展開時才注冊 document click handler,收起時移除 document click handler,是動態的。

    handleGlobalClick = () => {console.log('global click')this.setState({dropDownExpanded: false})document.removeEventListener('click', this.handleGlobalClick)} 復制代碼
  • 為了實現 toggle 的效果,即點擊按鈕,展開 dropdown menu,再點擊按鈕,則收到 menu,最簡單的辦法是,只有在 menu 收起的時候,才給按鈕綁定 click handler,menu 展開的時候,按鈕沒有 click handler,讓 document click handler 處理。否則,同時在合成事件的 handler 和原生事件的 handler 中調用 this.setState(),一個異步,一個同步,可能會引起麻煩。

    <div className="dropdown-head">{dropDownExpanded ?<button>Collapse dropdown menu - 1</button> :<button onClick={this.handleHeadClick}>Open dropdown menu - 1</button>}</div> 復制代碼
  • 注冊 document 的 click handler 時,必須在 setTimeout 回調中執行。

    handleHeadClick = () => {console.log('head click')this.setState({dropDownExpanded: true})setTimeout(()=>{// must run in the next tickdocument.addEventListener('click', this.handleGlobalClick)}, 0)} 復制代碼
  • 在 componentWillUnmount() 中要移除 document 的 click handler,以免造成內存泄漏。

    componentWillUnmount() {// important! we need remove global click handler when unmoutdocument.removeEventListener('click', this.handleGlobalClick)} 復制代碼
  • Update

    自從發現用 window.addEventListener('click', handler) 可以很方便地用來實現收起 React 中的 Dropdown 后,我就不亦樂乎的到處用起來了。為了避免寫無數遍的 window.addEventLister('click', handler),我封裝了一個 NativeClickListener 的 Component,代碼沒幾行,如下所示:

    export default class NativeClickListener extends React.Component {static propTypes = {onClick: PropTypes.func}clickHandler = (event) => {console.log('NativeClickListener click')const { onClick } = this.propsonClick && onClick(event)}componentDidMount() {window.addEventListener('click', this.clickHandler)}componentWillUnmount() {window.removeEventListener('click', this.clickHandler)}render() {return this.props.children} } 復制代碼

    使用:

    <div className="dropdown-container"><div className="dropdown-head"><button onClick={this.handleHeadClick}>{dropDownExpanded ? 'Collapse' : 'Open'} dropdown menu - 5</button></div>{dropDownExpanded &&<NativeClickListener onClick={()=>this.setState({dropDownExpanded: false})}><div className="dropdown-body"onClick={this.handleBodyClick}>...</div></NativeClickListener>} </div>handleHeadClick = (event) => {console.log('head click')this.setState(prevState => ({dropDownExpanded: !prevState.dropDownExpanded}))event.stopPropagation() } handleBodyClick = (event) => {console.log('body click')// just can stop event propagate from document to windowevent.stopPropagation() } 復制代碼

    后來我想,那其它開源的 React 組件庫中的 Dropdown 都是怎么實現的呢,于是探究了一下,果然不出意外,也是用的原生的 addEventListener 實現的,但也有點意外的是,它們并沒有用 window.addEventListener,而都是用了 document.addEventListener 和 node.contains 方法實現。

  • Material Kit React

    這個組件庫的 Dropdown 用到了 @material-ui/core/ClickAwayListener,來看看它的實現。

    handleClickAway = event => {...if (doc.documentElement &&doc.documentElement.contains(event.target) &&!this.node.contains(event.target)) {this.props.onClickAway(event);}}render() {const { children, mouseEvent, touchEvent, onClickAway, ...other } = this.props;const listenerProps = {};if (mouseEvent !== false) {listenerProps[mouseEvent] = this.handleClickAway;}if (touchEvent !== false) {listenerProps[touchEvent] = this.handleClickAway;}return (<React.Fragment>{children}<EventListener target="document" {...listenerProps} {...other} /></React.Fragment>);} 復制代碼

    addEventListener 的邏輯看來在 EventListener 中,來自 react-event-listener 庫。而且從 target="document" 來看,event 是綁在 document 上的。

    class EventListener extends React.PureComponent {componentDidMount() {this.applyListeners(on);}applyListeners(onOrOff, props = this.props) {const { target } = props;if (target) {let element = target;if (typeof target === 'string') {element = window[target];}forEachListener(props, onOrOff.bind(null, element));}...}function on(target, eventName, callback, options) {// eslint-disable-next-line prefer-spreadtarget.addEventListener.apply(target, getEventListenerArgs(eventName, callback, options));}function off(target, eventName, callback, options) {// eslint-disable-next-line prefer-spreadtarget.removeEventListener.apply(target, getEventListenerArgs(eventName, callback, options));} 復制代碼
  • Ant Design 中的 Dropdown 的實現最終可以追溯到 react-component/trigger 組件。

    // We must listen to `mousedown` or `touchstart`, edge case:// https://github.com/ant-design/ant-design/issues/5804// https://github.com/react-component/calendar/issues/250// https://github.com/react-component/trigger/issues/50if (state.popupVisible) {let currentDocument;if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextMenuToShow())) {currentDocument = props.getDocument();this.clickOutsideHandler = addEventListener(currentDocument,'mousedown', this.onDocumentClick);}// always hide on mobileif (!this.touchOutsideHandler) {currentDocument = currentDocument || props.getDocument();this.touchOutsideHandler = addEventListener(currentDocument,'touchstart', this.onDocumentClick);}// close popup when trigger type contains 'onContextMenu' and document is scrolling.if (!this.contextMenuOutsideHandler1 && this.isContextMenuToShow()) {currentDocument = currentDocument || props.getDocument();this.contextMenuOutsideHandler1 = addEventListener(currentDocument,'scroll', this.onContextMenuClose);}// close popup when trigger type contains 'onContextMenu' and window is blur.if (!this.contextMenuOutsideHandler2 && this.isContextMenuToShow()) {this.contextMenuOutsideHandler2 = addEventListener(window,'blur', this.onContextMenuClose);}return;}onDocumentClick = (event) => {if (this.props.mask && !this.props.maskClosable) {return;}const target = event.target;const root = findDOMNode(this);if (!contains(root, target) && !this.hasPopupMouseDown) {this.close();}} 復制代碼
  • JetBrain 的 ring-ui 的 Dropdown 并沒有實現在其它地方點擊后讓 Dropdown 收起的功能,有點意外...

  • 一開始不是很理解,不過后來我發現,如果用 window.addEventListener('click', handler) 的方式收起 Dropdown,在一個頁面中,如果有多個 Dropdown,我先展開一個 Dropdown menu (稱之為 A),再點擊另一個 Dropdown (稱之為 B),因為在 Dropdown B 的點擊事件中調用了 event.stopPropagation(),因此 Dropdown A 的 global click handler 將無法觸發,因此 Dropdown A 無法收起。

    即使只有一個 Dropdown,如果頁面中有其它任意地方的 event handler 中調用了 event.stopPropagation() 都會導致此 Dropdown 有可能無法收起。

    但是用 document.addEventListener('click', handler) 配合 node.contains() 方法卻不會有這個問題,因此恍然大悟,終于明白了為什么那些開源組件庫并沒有采用 window.addEventListener() 的方式。

    于是實現 NativeClickListener2:

    export default class NativeClickListener extends React.Component {static propTypes = {onClick: PropTypes.func}clickHandler = (event) => {console.log('NativeClickListener click')if(this._container.contains(event.target)) returnconst { onClick } = this.propsonClick && onClick(event)}componentDidMount() {document.addEventListener('click', this.clickHandler)}componentWillUnmount() {document.removeEventListener('click', this.clickHandler)}render() {return (<div ref={ref=>this._container=ref}>{this.props.children}</div>)} } 復制代碼

    使用:

    <div className="dropdown-container"><div className="dropdown-head"><button onClick={this.handleHeadClick}>{dropDownExpanded ? 'Collapse' : 'Open'} dropdown menu - 5</button></div>{dropDownExpanded &&<NativeClickListener2 onClick={()=>this.setState({dropDownExpanded: false})}><div className="dropdown-body"onClick={this.handleBodyClick}>...</div></NativeClickListener2>} </div>handleHeadClick = (event) => {console.log('head click')this.setState(prevState => ({dropDownExpanded: !prevState.dropDownExpanded}))// no need// event.stopPropagation() } handleBodyClick = (event) => {console.log('body click')// no need// event.stopPropagation() } 復制代碼

    總結

    以上是生活随笔為你收集整理的从 Dropdown 的 React 实现中学习到的的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 国产亚洲av综合人人澡精品 | 深夜精品福利 | 老妇free性videosxx| 国产91精品一区二区绿帽 | 插我舔内射18免费视频 | 大陆明星乱淫(高h)小说 | 亚洲人久久 | 色图综合网 | 青青草伊人久久 | 17c在线视频 | 亚洲一区二区三区蜜桃 | 好屌妞视频这里只有精品 | 777片理伦片在线观看 | 亚洲av无码久久忘忧草 | 亚洲成a人片77777kkkk | 欧美综合一区二区三区 | 天天操操操操 | 精品在线免费观看视频 | 久久丝袜美腿 | av噜噜 | 国产精品成人69xxx免费视频 | 爱福利视频一区 | 成人一区二区精品 | 久久人妻少妇嫩草av | 日本亲近相奷中文字幕 | 男女在线观看视频 | 人人玩人人干 | 亚洲男人天堂2018 | 欧美日韩性生活视频 | 91视频成人 | 天天插天天干 | 国产精品久久久久久婷婷天堂 | 三年在线观看视频 | 国产区在线| 日韩免费影院 | 亚洲少妇视频 | 一区二区在线视频观看 | 亚洲午夜精品视频 | 中国一级片黄色一级片黄 | 白白色2012年最新视频 | 国产综合视频在线观看 | 国产精品视频在线观看免费 | 四虎国产在线观看 | 538精品一线 | 中文字幕av免费在线观看 | 午夜三级av| 新香蕉视频 | 久久综合精品国产二区无码不卡 | 日韩欧美大片 | 五月天激情开心网 | 色呦呦一区二区三区 | 又黄又骚又爽 | 国产av电影一区 | 成年人免费黄色片 | 熟女少妇在线视频播放 | 亚洲精品一区二区潘金莲 | 91video| 欧美亚洲国产另类 | 男女性网站| 日本大尺度激情做爰hd | 99思思 | 娇妻玩4p被三个男人伺候电影 | 成人区人妻精品一区二区网站 | 美女黄色一级 | 日韩乱码一区二区三区 | 男女午夜免费视频 | 日韩网站免费 | 亚洲AV综合色区国产精品天天 | aaa天堂| 99资源在线 | 绿帽h啪肉np辣文 | 亚洲成在线观看 | 亚洲香蕉在线观看 | 最近国语视频在线观看免费播放 | 国产有码 | 九热精品 | 综合在线亚洲 | 亚洲第一区视频 | 国产中文久久 | 最新av在线 | 精品中文字幕在线播放 | 欧洲在线视频 | 亚洲AV无码精品黑人黑人 | 咪咪色影院 | 天天视频污 | 亚州激情视频 | 国产婷婷一区二区三区久久 | 国产毛片一区二区三区 | 亚洲理论片 | 熟女少妇精品一区二区 | 久久久久久国产精品三级玉女聊斋 | 亚洲女人被黑人巨大进入 | 国产福利小视频在线 | 色网站在线播放 | 国产av无码专区亚洲a∨毛片 | 欧美成人黄 | 亚洲一区二区视频网站 | www国产精品 | 欧美精品色视频 |