1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        深入學(xué)習(xí) React 合成事件

        共 5728字,需瀏覽 12分鐘

         ·

        2020-10-16 02:08

        翁斌斌,微醫(yī)云前端工程師,在程序員的修煉道路上永不止步。

        以下分析基于React, ReactDOM 16.13.1版本

        提出問題

        我們借鑒一個(gè)比較典型的案例開始來分析React事件

        export?default?class?Dialog?extends?React.PureComponent?{
        ??state?=?{
        ????showBox:?false
        ??};
        ??componentDidMount()?{
        ????document.addEventListener("click",?this.handleClickBody,?false);
        ??}
        ??handleClickBody?=?()?=>?{
        ????this.setState({
        ??????showBox:?false
        ????});
        ??};
        ??handleClickButton?=?(e)?=>?{
        ????e.nativeEvent.stopPropagation();
        ????this.setState({
        ??????showBox:?true
        ????});
        ??};
        ??render()?{
        ????return?(
        ??????<div>
        ????????<button?onClick={this.handleClickButton}>點(diǎn)擊我顯示彈窗button>

        ????????{this.state.showBox?&&?(
        ??????????<div?onClick={(e)?=>?e.nativeEvent.stopPropagation()}>我是彈窗div>
        ????????)}
        ??????div>
        ????);
        ??}
        }

        從上面的代碼里我們不難看出我們想要做一個(gè)點(diǎn)擊某一個(gè)按鈕來展示一個(gè)模態(tài)框,并且在點(diǎn)擊除了模態(tài)框區(qū)域以外的位置希望能夠關(guān)閉這個(gè)模態(tài)框。 但是實(shí)際運(yùn)行結(jié)果和我們所想的完全不一樣,點(diǎn)擊了button按鈕并沒有任何反應(yīng),這就需要從React的合成事件說起了,讓我們分析完React的合成事件 后能夠完全的來解答這個(gè)問題。

        demo地址:https://codesandbox.io/s/event-uww15?file=/src/App.tsx:0-690

        合成事件的特性

        React自行實(shí)現(xiàn)了一套事件系統(tǒng),主要特性有以下

        1. 自行實(shí)現(xiàn)了一套事件捕獲到事件冒泡的邏輯, 抹平各個(gè)瀏覽器之前的兼容性問題。
        2. 使用對(duì)象池來管理合成事件對(duì)象的創(chuàng)建和銷毀,可以減少垃圾回收次數(shù),防止內(nèi)存抖動(dòng)。
        3. 事件只在document上綁定,并且每種事件只綁定一次,減少內(nèi)存開銷。

        首先我們先拋開上面那個(gè)按鈕,用下面這個(gè)十分簡(jiǎn)單的案例來了解React的事件使用。

        function?App()?{
        ??function?handleButtonLog(e:?React.MouseEvent)?{
        ????console.log(e.currentTarget);
        ??}
        ??function?handleDivLog(e:?React.MouseEvent)?{
        ????console.log(e.currentTarget);
        ??}
        ??function?handleH1Log(e:?React.MouseEvent)?{
        ????console.log(e.currentTarget);
        ??}
        ??return?(
        ????<div?onClick={handleDivLog}>
        ??????<h1?onClick={handleH1Log}>
        ????????<button?onClick={handleButtonLog}>clickbutton>

        ??????h1>
        ????div>
        ??);
        }

        上面的代碼運(yùn)行后,會(huì)在控制臺(tái)中分別打印出,button, h1, div三個(gè)dom節(jié)點(diǎn),我們來研究一下他是如何工作的。

        事件綁定

        首先來確認(rèn)事件是如何綁定到dom節(jié)點(diǎn)上的,我們知道App組件內(nèi)的jsx代碼會(huì)通過React.CreateElement函數(shù)返回jsx對(duì)象,其中我們的onClick事件是儲(chǔ)存在每一個(gè)jsx對(duì)象props屬性內(nèi),通過一系列方法得知在Reactreconciliation階段中會(huì)把jsx對(duì)象轉(zhuǎn)換為fiber對(duì)象,這里有一個(gè)方法叫做completeWork

        function?completeWork(current,?workInProgress,?renderExpirationTime)?{
        ????//?只保留關(guān)鍵代碼
        ????case?HostComponent:
        ??????{
        ????????popHostContext(workInProgress);
        ????????var?rootContainerInstance?=?getRootHostContainer();
        ????????var?type?=?workInProgress.type;
        ????????if?(current?!==?null?&&?workInProgress.stateNode?!=?null)?{
        ??????????//?更新
        ????????}?else?{
        ??????????//?創(chuàng)建
        ??????????if?(_wasHydrated)?{
        ????????????//?ssr情況
        ??????????}?else?{
        ????????????var?instance?=?createInstance(type,?newProps,?rootContainerInstance,?currentHostContext,?workInProgress);

        ????????????//?初始化DOM節(jié)點(diǎn)
        ????????????if?(finalizeInitialChildren(instance,?type,?newProps,?rootContainerInstance))?{
        ????????????}
        ??????????}
        ????????}
        }

        這個(gè)函數(shù)內(nèi)通過createInstance創(chuàng)建dom實(shí)例,并且調(diào)用finalizeInitialChildren函數(shù),在finalizeInitialChildren函數(shù)中會(huì)把props設(shè)置到真實(shí)的dom節(jié)點(diǎn)上,這里如果遇到類似onClick,onChange的props時(shí),會(huì)觸發(fā)事件綁定的邏輯。

        //?進(jìn)行事件綁定
        ensureListeningTo(rootContainerElement,?propKey);

        function?ensureListeningTo(rootContainerElement,?registrationName)?{
        ??//?忽略無關(guān)代碼
        ??var?doc?=?isDocumentOrFragment???rootContainerElement?:?rootContainerElement.ownerDocument;
        ??legacyListenToEvent(registrationName,?doc);
        }

        ensureListeningTo函數(shù)中會(huì)通過實(shí)際觸發(fā)事件的節(jié)點(diǎn),去尋找到它的document節(jié)點(diǎn),并且調(diào)用legacyListenToEvent函數(shù)來進(jìn)行事件綁定

        function?legacyListenToEvent(registrationName,?mountAt)?{
        ??var?listenerMap?=?getListenerMapForElement(mountAt);
        ??var?dependencies?=?registrationNameDependencies[registrationName];

        ??for?(var?i?=?0;?i?????var?dependency?=?dependencies[i];
        ????legacyListenToTopLevelEvent(dependency,?mountAt,?listenerMap);
        ??}
        }

        registrationNameDependencies數(shù)據(jù)結(jié)構(gòu)如下

        legacyListenToEvent函數(shù)中首先通過獲取document節(jié)點(diǎn)上監(jiān)聽的事件名稱Map對(duì)象,然后去通過綁定在jsx上的事件名稱,例如onClick來獲取到真實(shí)的事件名稱,例如click,依次進(jìn)行legacyListenToTopLevelEvent方法的調(diào)用

        function?legacyListenToTopLevelEvent(topLevelType,?mountAt,?listenerMap)?{
        ??//?只保留主邏輯
        ??//?相同的事件只綁定一次
        ??if?(!listenerMap.has(topLevelType))?{
        ????switch?(topLevelType)?{
        ??????//?根據(jù)事件類型進(jìn)行捕獲或者冒泡綁定
        ??????case?TOP_SCROLL:
        ????????trapCapturedEvent(XX);
        ??????default:
        ????????trapBubbledEvent(topLevelType,?mountAt)
        ????????break;
        ????}

        ????listenerMap.set(topLevelType,?null);
        ??}
        }
        //?無論是trapBubbledEvent還是trapCapturedEvent都是調(diào)用trapEventForPluginEventSystem
        //?區(qū)別就是第三個(gè)參數(shù)是ture還是false用來對(duì)應(yīng)addEventListener中的第三個(gè)參數(shù)
        function?trapBubbledEvent(topLevelType,?element)?{
        ??trapEventForPluginEventSystem(element,?topLevelType,?false);
        }
        function?trapCapturedEvent(topLevelType,?element)?{
        ??trapEventForPluginEventSystem(element,?topLevelType,?true);
        }

        legacyListenToTopLevelEvent函數(shù)做了以下兩件事

        1. 是否在document上已經(jīng)綁定過原始事件名,已經(jīng)綁定過則直接退出,未綁定則綁定結(jié)束以后把事件名稱設(shè)置到Map對(duì)象上,再下一次綁定相同的事件時(shí)直接跳過。
        2. 根據(jù)事件是否能冒泡來來進(jìn)行捕獲階段的綁定或者冒泡階段的綁定。
        function?trapEventForPluginEventSystem(container,?topLevelType,?capture)?{
        ??var?listener;

        ??switch?(getEventPriorityForPluginSystem(topLevelType))?{
        ????case?DiscreteEvent:
        ??????listener?=?dispatchDiscreteEvent.bind(null,?topLevelType,?PLUGIN_EVENT_SYSTEM,?container);
        ??????break;

        ????case?UserBlockingEvent:
        ??????listener?=?dispatchUserBlockingUpdate.bind(null,?topLevelType,?PLUGIN_EVENT_SYSTEM,?container);
        ??????break;

        ????case?ContinuousEvent:
        ????default:
        ??????listener?=?dispatchEvent.bind(null,?topLevelType,?PLUGIN_EVENT_SYSTEM,?container);
        ??????break;
        ??}

        ??var?rawEventName?=?getRawEventName(topLevelType);

        ??if?(capture)?{
        ????addEventCaptureListener(container,?rawEventName,?listener);
        ??}?else?{
        ????addEventBubbleListener(container,?rawEventName,?listener);
        ??}
        }

        到目前為止我們已經(jīng)拿到了真實(shí)的事件名稱和綁定在事件的哪個(gè)階段,剩下就還有一個(gè)監(jiān)聽事件本身了,這一步會(huì)在trapEventForPluginEventSystem函數(shù)內(nèi)被獲取到,他會(huì)通過事件的優(yōu)先級(jí)來獲取不同的監(jiān)聽事件,這部分會(huì)和調(diào)度方面有相關(guān),我們只需要知道最終實(shí)際綁定的都是dispatchEvent這個(gè)監(jiān)聽事件,然后調(diào)用瀏覽器的addEventListener事件來綁定上dispatchEvent函數(shù)

        到此為止事件的綁定暫時(shí)告一段落了,從上面能得出幾個(gè)結(jié)論。

        1. 事件都是綁定在document上的。
        2. jsx中的事件名稱會(huì)經(jīng)過處理,處理后的事件名稱才會(huì)被綁定,例如onClick會(huì)使用click這個(gè)名稱來綁定。
        3. 不管用什么事件來綁定, 他們的監(jiān)聽事件并不是傳入jsx的事件函數(shù),而是會(huì)根據(jù)事件的優(yōu)先級(jí)來綁定dispatchDiscreteEvent,dispatchUserBlockingUpdate或者dispatchEvent三個(gè)監(jiān)聽函數(shù)之一,但是最終在觸發(fā)事件調(diào)用的還是dispatchEvent事件。

        事件觸發(fā)

        從事件綁定得知我們點(diǎn)擊的button按鈕的時(shí)候,觸發(fā)的回調(diào)函數(shù)并不是實(shí)際的回調(diào)函數(shù),而是dispatchEvent函數(shù), 所以我們通常會(huì)有幾個(gè)疑問

        1. 它是怎么獲取到用戶事件的回調(diào)函數(shù)的?
        2. 為什么在合成事件對(duì)象不能被保存下來,而需要調(diào)用特殊的函數(shù)才能保留?
        3. 合成事件是怎么創(chuàng)建出來的?
        function?dispatchEventForLegacyPluginEventSystem(topLevelType,?eventSystemFlags,?nativeEvent,?targetInst)?{
        ??var?bookKeeping?=?getTopLevelCallbackBookKeeping(topLevelType,?nativeEvent,?targetInst,?eventSystemFlags);
        ??try?{
        ????batchedEventUpdates(handleTopLevel,?bookKeeping);
        ??}?finally?{
        ????releaseTopLevelCallbackBookKeeping(bookKeeping);
        ??}
        }

        接下來的分析中我們就來解決這幾個(gè)問題,首先看到dispatchEvent函數(shù),忽略掉其他分支會(huì)發(fā)現(xiàn)實(shí)際調(diào)用的是dispatchEventForLegacyPluginEventSystem函數(shù), 他首先通過callbackBookkeepingPool中獲取一個(gè)bookKeeping對(duì)象,然后調(diào)用handleTopLevel函數(shù),在調(diào)用結(jié)束的時(shí)候吧bookKeeping對(duì)象放回到callbackBookkeepingPool中,實(shí)現(xiàn)了內(nèi)存復(fù)用。

        bookKeeping對(duì)象的結(jié)構(gòu)如圖

        //?忽略分支代碼,只保留主流程
        function?handleTopLevel(bookKeeping)?{
        ??var?targetInst?=?bookKeeping.targetInst;
        ??var?ancestor?=?targetInst;
        ??do?{
        ????var?tag?=?ancestor.tag;
        ????if?(tag?===?HostComponent?||?tag?===?HostText)?{
        ??????bookKeeping.ancestors.push(ancestor);
        ????}
        ??}?while?(ancestor);

        ??for?(var?i?=?0;?i?????targetInst?=?bookKeeping.ancestors[i];

        ????runExtractedPluginEventsInBatch(topLevelType,?targetInst,?nativeEvent,?eventTarget,?eventSystemFlags);
        ??}
        }

        handleTopLevel函數(shù)內(nèi),通過首先把觸發(fā)事件的節(jié)點(diǎn)如果是dom節(jié)點(diǎn)或者文字節(jié)點(diǎn)的話,那就把對(duì)應(yīng)的fiber對(duì)象放入bookkeeping.ancestors的數(shù)組內(nèi),接下去依次獲取bookKeeping.ancestors上的每一個(gè)fiber對(duì)象,通過runExtractedPluginEventsInBatch函數(shù)來創(chuàng)建合成事件對(duì)象。

        function?runExtractedPluginEventsInBatch(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags)?{
        ??var?events?=?extractPluginEvents(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags);
        ??runEventsInBatch(events);
        }

        runExtractedPluginEventsInBatch中會(huì)通過調(diào)用extractPluginEvents函數(shù),在這個(gè)函數(shù)內(nèi)通過targetInst這個(gè)fiber對(duì)象,從這個(gè)對(duì)象一直往上尋找,尋找有一樣的事件綁定的節(jié)點(diǎn),并且把他們的回調(diào)事件組合到合成事件對(duì)象上,這里先討論事件觸發(fā)的流程,所以先簡(jiǎn)單帶過合成事件是如何生成的以及是如何去尋找到需要被觸發(fā)的事件, 后面會(huì)詳細(xì)的講解合成事件,最后在拿到合成事件以后調(diào)用runEventsInBatch函數(shù)

        function?runEventsInBatch(events)?{
        ??forEachAccumulated(processingEventQueue,?executeDispatchesAndReleaseTopLevel);
        }

        其中processingEventQueue是多個(gè)事件列表,我們這只有一個(gè)事件隊(duì)列,forEachAccumulated它的目的是為了按照隊(duì)列的順序去執(zhí)行多個(gè)事件,在我們的例子中其實(shí)就相當(dāng)于executeDispatchesAndReleaseTopLevel(processingEventQueue),接下來就是調(diào)用到executeDispatchesAndRelease,從名稱就看出來他是首先執(zhí)行事件,然后對(duì)事件對(duì)象進(jìn)行釋放

        var?executeDispatchesAndRelease?=?function?(event)?{
        ??if?(event)?{
        ????executeDispatchesInOrder(event);
        ????if?(!event.isPersistent())?{
        ??????event.constructor.release(event);
        ????}
        ??}
        };

        代碼很少,首先調(diào)用executeDispatchesInOrder來傳入合成事件,在里面按照順序去執(zhí)行合成事件對(duì)象上的回調(diào)函數(shù),如果有多個(gè)回調(diào)函數(shù),在執(zhí)行每個(gè)回調(diào)函數(shù)的時(shí)候還會(huì)去判斷event.isPropagationStopped()的狀態(tài),之前有函數(shù)調(diào)用了合成事件的stopPropagation函數(shù)的話,就停止執(zhí)行后續(xù)的回調(diào),但是要注意的時(shí)候這里的dispatchListeners[i]函數(shù)并不是用戶傳入的回調(diào)函數(shù),而是經(jīng)過包裝的事件,這塊會(huì)在合成事件的生成中介紹,在事件執(zhí)行結(jié)束后React還會(huì)去根據(jù)用戶是否調(diào)用了event.persist()函數(shù)來決定是否保留這次的事件對(duì)象是否要回歸事件池,如果未被調(diào)用,該事件對(duì)象上的狀態(tài)會(huì)被重置,至此事件觸發(fā)已經(jīng)完畢。

        合成事件的生成

        從事件監(jiān)聽的流程中我們知道了合成事件是從extractPluginEvents創(chuàng)建出來的,那么看一下extractPluginEvents的代碼

        function?extractPluginEvents(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags)?{
        ??var?events?=?null;
        ??for?(var?i?=?0;?i?????var?possiblePlugin?=?plugins[i];
        ????if?(possiblePlugin)?{
        ??????var?extractedEvents?=?possiblePlugin.extractEvents(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags);
        ??????if?(extractedEvents)?{
        ????????events?=?accumulateInto(events,?extractedEvents);
        ??????}
        ????}
        ??}
        ??return?events;
        }

        首先來了解一下plugins是個(gè)什么東西,由于React會(huì)服務(wù)于不同的平臺(tái),所以每個(gè)平臺(tái)的事件會(huì)用插件的形式來注入到React中,例如瀏覽器就是ReactDOM中進(jìn)行注入

        injectEventPluginsByName({
        ??SimpleEventPlugin:?SimpleEventPlugin,
        ??EnterLeaveEventPlugin:?EnterLeaveEventPlugin,
        ??ChangeEventPlugin:?ChangeEventPlugin,
        ??SelectEventPlugin:?SelectEventPlugin,
        ??BeforeInputEventPlugin:?BeforeInputEventPlugin,
        });

        injectEventPluginsByName函數(shù)會(huì)通過一些操作把事件插件注冊(cè)到plugins對(duì)象上,數(shù)據(jù)結(jié)構(gòu)如下

        所以會(huì)依次遍歷plugin,調(diào)用plugin上的extractEvents函數(shù)來嘗試是否能夠生成出合成事件對(duì)象,在我們的例子中用的是click事件,那么它會(huì)進(jìn)入到SimpleEventPlugin.extractEvents函數(shù)

        var?SimpleEventPlugin?=?{
        ??extractEvents:?function?(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags)?{
        ????var?EventConstructor;
        ????switch?(topLevelType)?{
        ??????case?TOP_KEY_DOWN:
        ??????case?TOP_KEY_UP:
        ????????EventConstructor?=?SyntheticKeyboardEvent;
        ????????break;

        ??????case?TOP_BLUR:
        ??????case?TOP_FOCUS:
        ????????EventConstructor?=?SyntheticFocusEvent;
        ????????break;

        ??????default:
        ????????EventConstructor?=?SyntheticEvent;
        ????????break;
        ????}

        ????var?event?=?EventConstructor.getPooled(dispatchConfig,?targetInst,?nativeEvent,?nativeEventTarget);
        ????accumulateTwoPhaseDispatches(event);
        ????return?event;
        ??}
        };

        這個(gè)函數(shù)是通過topLevelType的類型來獲取合成事件的構(gòu)造函數(shù),例如代碼中的SyntheticKeyboardEvent,SyntheticFocusEvent等都是SyntheticEvent的子類,在基礎(chǔ)上附加了自己事件的特殊屬性,我們的click事件會(huì)使用到SyntheticEvent這個(gè)構(gòu)造函數(shù),然后通過getPooled函數(shù)來創(chuàng)建或者從事件池中取出一個(gè)合成事件對(duì)象實(shí)例。然后在accumulateTwoPhaseDispatchesSingle函數(shù)中,按照捕獲到冒泡的順序來獲取所有的事件回調(diào)

        function?accumulateTwoPhaseDispatchesSingle(event)?{
        ??if?(event?&&?event.dispatchConfig.phasedRegistrationNames)?{
        ????traverseTwoPhase(event._targetInst,?accumulateDirectionalDispatches,?event);
        ??}
        }

        function?traverseTwoPhase(inst,?fn,?arg)?{
        ??var?path?=?[];

        ??while?(inst)?{
        ????path.push(inst);
        ????inst?=?getParent(inst);
        ??}

        ??var?i;

        ??for?(i?=?path.length;?i--?>?0;)?{
        ????fn(path[i],?'captured',?arg);
        ??}

        ??for?(i?=?0;?i?????fn(path[i],?'bubbled',?arg);
        ??}
        }

        traverseTwoPhase函數(shù)會(huì)從當(dāng)前的fiber節(jié)點(diǎn)通過return屬性,找到所有的是原生DOM節(jié)點(diǎn)的fiber對(duì)象,然后推入到列表中,我們的例子中就是[ButtonFiber, H1Fiber, DivFiber], 首先執(zhí)行捕獲階段的循環(huán),從后往前執(zhí)行,接著從前往后執(zhí)行冒泡的循環(huán),對(duì)應(yīng)了瀏覽器原始的事件觸發(fā)流程,最后會(huì)往accumulateDirectionalDispatches函數(shù)中傳入當(dāng)前執(zhí)行的fiber和事件執(zhí)行的階段。

        function?listenerAtPhase(inst,?event,?propagationPhase)?{
        ??var?registrationName?=?event.dispatchConfig.phasedRegistrationNames[propagationPhase];
        ??return?getListener(inst,?registrationName);
        }

        function?accumulateDirectionalDispatches(inst,?phase,?event)?{
        ??var?listener?=?listenerAtPhase(inst,?event,?phase);
        ??if?(listener)?{
        ????event._dispatchListeners?=?accumulateInto(event._dispatchListeners,?listener);
        ????event._dispatchInstances?=?accumulateInto(event._dispatchInstances,?inst);
        ??}
        }

        listenerAtPhase中首先通過原生事件名和當(dāng)前執(zhí)行的階段(捕獲,還是冒泡)去再去獲取對(duì)應(yīng)的props事件名稱(onClick,onClickCapture),然后通過React事件名稱去fiber節(jié)點(diǎn)上獲取到相應(yīng)的事件回調(diào)函數(shù),最后拼接在合成對(duì)象的_dispatchListeners數(shù)組內(nèi),當(dāng)全部節(jié)點(diǎn)運(yùn)行結(jié)束以后_dispatchListeners對(duì)象上就會(huì)有三個(gè)回調(diào)函數(shù)[handleButtonLog, handleH1Log, handleDivLog],這里的回調(diào)函數(shù)就是我們?cè)诮M件內(nèi)定義的真實(shí)事件的回調(diào)函數(shù)。

        到此合成事件構(gòu)造就完成了,主要做了三件事:

        1. 通過事件名稱去選擇合成事件的構(gòu)造函數(shù),
        2. 事件去獲取到組件上事件綁定的回調(diào)函數(shù)設(shè)置到合成事件上的_dispatchListeners屬性上,用于事件觸發(fā)的時(shí)候去調(diào)用。
        3. 還有就是在初始化的時(shí)候去注入平臺(tái)的事件插件。

        事件解綁

        通常我們寫事件綁定的時(shí)候會(huì)在頁(yè)面卸載的時(shí)候進(jìn)行事件的解綁,但是在React中,框架本身由于只會(huì)在document上進(jìn)行每種事件最多一次的綁定,所以并不會(huì)進(jìn)行事件的解綁。

        批量更新

        當(dāng)然如果我們使用React提供的事件,而不是使用我們自己綁定的原生事件除了會(huì)進(jìn)行事件委托以外還有什么優(yōu)勢(shì)呢? 再來看一個(gè)例子

        export?default?class?EventBatchUpdate?extends?React.PureComponent<>?{
        ??button?=?null;
        ??constructor(props)?{
        ????super(props);
        ????this.state?=?{
        ??????count:?0
        ????};
        ????this.button?=?React.createRef();
        ??}
        ??componentDidMount()?{
        ????this.button.current.addEventListener(
        ??????"click",
        ??????this.handleNativeClickButton,
        ??????false
        ????);
        ??}
        ??handleNativeClickButton?=?()?=>?{
        ????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
        ????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
        ??};
        ??handleClickButton?=?()?=>?{
        ????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
        ????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
        ??};
        ??render()?{
        ????console.log("update");
        ????return?(
        ??????<div>
        ????????<h1>legacy?eventh1>

        ????????<button?ref={this.button}>native?event?addbutton>
        ????????<button?onClick={this.handleClickButton}>React?event?addbutton>
        ????????{this.state.count}
        ??????div>
        ????);
        ??}
        }

        在線demo地址:https://codesandbox.io/s/legacy-event-kjngx?file=/src/App.tsx:0-1109

        首先點(diǎn)擊第一個(gè)按鈕,發(fā)現(xiàn)有兩個(gè)update被打印出,意味著被render了兩次。

        點(diǎn)擊第二個(gè)按鈕,只有一個(gè)update被打印出來。

        會(huì)發(fā)現(xiàn)通過React事件內(nèi)多次調(diào)用setState,會(huì)自動(dòng)合并多個(gè)setState,但是在原生事件綁定上默認(rèn)并不會(huì)進(jìn)行合并多個(gè)setState,那么有什么手段能解決這個(gè)問題呢?

        1. 通過batchUpdate函數(shù)來手動(dòng)聲明運(yùn)行上下文。
        ??handleNativeClickButton?=?()?=>?{
        ????ReactDOM.unstable_batchedUpdates(()?=>?{
        ??????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
        ??????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
        ????});
        ??};

        在線demo地址:https://codesandbox.io/s/legacy-eventbatchupdate-smisq?file=/src/App.tsx:519-749

        首先點(diǎn)擊第一個(gè)按鈕,只有一個(gè)update被打印出來。

        點(diǎn)擊第二個(gè)按鈕,還是只有一個(gè)update被打印出來。

        1. 啟用concurrent mode的情況。(目前不推薦,未來的方案)
        import?ReactDOM?from?"React-dom";

        const?root?=?ReactDOM.unstable_createRoot(document.getElementById("root"));
        root.render(<App?/>);

        在線demo地址:https://codesandbox.io/s/concurrentevent-9oxoi?file=/src/index.js:0-224

        會(huì)發(fā)現(xiàn)不需要修改任何代碼,只需要開啟concurrent mode,就會(huì)自動(dòng)進(jìn)行setState的合并。

        首先點(diǎn)擊第一個(gè)按鈕,只有一個(gè)update被打印出來。

        點(diǎn)擊第二個(gè)按鈕,還是只有一個(gè)update被打印出來。

        React17中的事件改進(jìn)

        在最近發(fā)布的React17版本中,對(duì)事件系統(tǒng)了一些改動(dòng),和16版本里面的實(shí)現(xiàn)有了一些區(qū)別,我們就來了解一下17中更新的點(diǎn)。

        1. 更改事件委托
        • 首先第一個(gè)修改點(diǎn)就是更改了事件委托綁定節(jié)點(diǎn),在16版本中,React都會(huì)把事件綁定到頁(yè)面的document元素上,這在多個(gè)React版本共存的情況下就會(huì)雖然某個(gè)節(jié)點(diǎn)上的函數(shù)調(diào)用了e.stopPropagation(),但還是會(huì)導(dǎo)致另外一個(gè)React版本上綁定的事件沒有被阻止觸發(fā),所以在17版本中會(huì)把事件綁定到render函數(shù)的節(jié)點(diǎn)上。
        1. 去除事件池
        • 17版本中移除了event pooling,這是因?yàn)?React 在舊瀏覽器中重用了不同事件的事件對(duì)象,以提高性能,并將所有事件字段在它們之前設(shè)置為 null。在 React 16 及更早版本中,使用者必須調(diào)用 e.persist() 才能正確的使用該事件,或者正確讀取需要的屬性。
        1. 對(duì)標(biāo)瀏覽器
        • onScroll 事件不再冒泡,以防止出現(xiàn)常見的混淆。
        • React 的 onFocusonBlur 事件已在底層切換為原生的 focusinfocusout 事件。它們更接近 React 現(xiàn)有行為,有時(shí)還會(huì)提供額外的信息。
        • 捕獲事件(例如,onClickCapture)現(xiàn)在使用的是實(shí)際瀏覽器中的捕獲監(jiān)聽器。

        問題解答

        現(xiàn)在讓我們回到最開始的例子中,來看這個(gè)問題如何被修復(fù)

        . ?16版本修復(fù)方法一

        ??handleClickButton?=?(e:?React.MouseEvent)?=>?{
        ????e.nativeEvent.stopImmediatePropagation();
        ????...
        ??};

        我們知道React事件綁定的時(shí)刻是在reconciliation階段,會(huì)在原生事件的綁定前,那么可以通過調(diào)用e.nativeEvent.stopImmediatePropagation(); 來進(jìn)行document后續(xù)事件的阻止。

        在線demo地址:https://codesandbox.io/s/v16fixevent1-wb8m7

        • 16版本修復(fù)方法二
        ??window.addEventListener("click",?this.handleClickBody,?false);

        另外一個(gè)方法就是在16版本中事件會(huì)被綁定在document上,所以只要把原生事件綁定在window上,并且調(diào)用e.nativeEvent.stopPropagation();來阻止事件冒泡到window上即可修復(fù)。

        在線demo地址:https://codesandbox.io/s/v16fixevent2-4e2b5

        • React17版本修復(fù)方法

        在17版本中React事件并不會(huì)綁定在document上,所以并不需要修改任何代碼,即可修復(fù)這個(gè)問題。

        在線demo地址:https://codesandbox.io/s/v17fixevent-wzsw5

        總結(jié)

        我們通過一個(gè)經(jīng)典的例子入手,自頂而下來分析React源碼中事件的實(shí)現(xiàn)方式,了解事件的設(shè)計(jì)思想,最后給出多種的解決方案,能夠在繁雜的業(yè)務(wù)中挑選最合適的技術(shù)方案來進(jìn)行實(shí)踐。

        瀏覽 46
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            日韩mv欧美mv国产网站 | 男人操女人下面的视频 | 无码人妻精品一区二区三区9厂 | 久久中文字幕7区 | 欧美日日摸夜夜添夜夜添 | 亚洲免费人成在线观看网站 | 免费在线成人毛片 | 久久久久久国产精品美女 | 影音先锋sv电影资源 | 老师的逼好紧 |