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>

        「React進階」一文吃透react事件原理

        共 36989字,需瀏覽 74分鐘

         ·

        2021-05-01 10:37



        一 前言

        今天我們來一起探討一下React事件原理,這篇文章,我盡量用通俗簡潔的方式,把React事件系統(tǒng)講的明明白白。

        我們講的react版本是16.13.1 , v17之后react對于事件系統(tǒng)會有相關(guān)的改版,文章后半部分會提及。

        老規(guī)矩,在正式講解react之前,我們先想想這幾個問題(如果我是面試官,你會怎么回答?):

        • 1 我們寫的事件是綁定在dom上么,如果不是綁定在哪里?
        • 2 為什么我們的事件不能綁定給組件?
        • 3 為什么我們的事件手動綁定this(不是箭頭函數(shù)的情況)
        • 4 為什么不能用 return false來阻止事件的默認行為?
        • 5 react怎么通過dom元素,找到與之對應(yīng)的 fiber對象的?
        • 6 onClick是在冒泡階段綁定的?那么onClickCapture就是在事件捕獲階段綁定的嗎?
        B7836791-2C40-48BA-83BF-835E0BD87B55.jpg

        必要的知識概念

        在弄清楚react事件之前,有幾個概念我們必須弄清楚,因為只有弄明白這幾個概念,在事件觸發(fā)階段,我們才能更好的理解react處理事件本質(zhì)。

        我們寫在JSX事件終將變成什么?

        我們先寫一段含有點擊事件的react JSX語法,看一下它最終會變成什么樣子?

        class Index extends React.Component{
            handerClick= (value) => console.log(value) 
            render(){
                return <div>
                    <button onClick={ this.handerClick } > 按鈕點擊 </button>
                </div>

            }
        }

        經(jīng)過babel轉(zhuǎn)換成React.createElement形式,如下:

        babel.jpg

        最終轉(zhuǎn)成fiber對象形式如下:

        fiber.jpg

        fiber對象上的memoizedPropspendingProps保存了我們的事件。

        什么是合成事件?

        通過上一步我們看到了,我們聲明事件保存的位置。但是事件有沒有被真正的注冊呢?我們接下來看一下:

        我們看一下當(dāng)前這個元素<button>上有沒有綁定這個事件監(jiān)聽器呢?

        button_event.jpg

        button上綁定的事件

        我們可以看到 ,button上綁定了兩個事件,一個是document上的事件監(jiān)聽器,另外一個是button,但是事件處理函數(shù)handle,并不是我們的handerClick事件,而是noop。

        noop是什么呢?我們接著來看。

        原來noop就指向一個空函數(shù)。

        noop.jpg

        然后我們看document綁定的事件

        document.jpg

        可以看到click事件被綁定在document上了。

        接下來我們再搞搞事情??????,在demo項目中加上一個input輸入框,并綁定一個onChange事件。睜大眼睛看看接下來會發(fā)生什么?

        class Index extends React.Component{
            componentDidMount(){
                console.log(this)
            }
            handerClick= (value) => console.log(value) 
            handerChange=(value) => console.log(value)
            render(){
                return <div style={{ marginTop:'50px' }} >
                    <button onClick={ this.handerClick } > 按鈕點擊 </button>
                    <input  placeholder="請輸入內(nèi)容" onChange={ this.handerChange }  />
                </div>

            }
        }

        我們先看一下input dom元素上綁定的事件

        22BEC470-233A-4C50-9C47-D21D343C055D.jpg

        然后我們看一下document上綁定的事件

        8E1D3BDB-ACFB-4E49-A5FF-CF990C47A60E.jpg

        我們發(fā)現(xiàn),我們給<input>綁定的onChange,并沒有直接綁定在input上,而是統(tǒng)一綁定在了document上,然后我們onChange被處理成很多事件監(jiān)聽器,比如blur , change , input , keydown , keyup 等。

        綜上我們可以得出結(jié)論:

        • 我們在 jsx 中綁定的事件(demo中的handerClick,handerChange),根本就沒有注冊到真實的dom上。是綁定在document上統(tǒng)一管理的。

        • 真實的dom上的click事件被單獨處理,已經(jīng)被react底層替換成空函數(shù)。

        • 我們在react綁定的事件,比如onChange,在document上,可能有多個事件與之對應(yīng)。

        • react并不是一開始,把所有的事件都綁定在document上,而是采取了一種按需綁定,比如發(fā)現(xiàn)了onClick事件,再去綁定document click事件。

        那么什么是react事件合成呢?

        react中,我們綁定的事件onClick等,并不是原生事件,而是由原生事件合成的React事件,比如 click事件合成為onClick事件。比如blur , change , input , keydown , keyup等 , 合成為onChange

        那么react采取這種事件合成的模式呢?

        一方面,將事件綁定在document統(tǒng)一管理,防止很多事件直接綁定在原生的dom元素上。造成一些不可控的情況

        另一方面, React 想實現(xiàn)一個全瀏覽器的框架, 為了實現(xiàn)這種目標(biāo)就需要提供全瀏覽器一致性的事件系統(tǒng),以此抹平不同瀏覽器的差異。

        接下來的文章中,會介紹react是怎么做事件合成的。

        dom元素對應(yīng)的fiber Tag對象

        我們知道了react怎么儲存了我們的事件函數(shù)和事件合成因果。接下來我想讓大家記住一種類型的 fiber 對象,因為后面會用到,這對后續(xù)的理解很有幫助。

        我們先來看一個代碼片段:

        <div> 
          <div> hello , my name is alien </div>
        </div>

        <div> hello , my name is alien </div> 對應(yīng)的 fiber類型。tag = 5

        然后我們?nèi)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">react源碼中找到這種類的fiber類型。

        /react-reconciler/src/ReactWorkTagsq.js

        export const HostComponent = 5// 元素節(jié)點

        好的 ,我們暫且把 HostComponentHostText記錄??下來。接下來回歸正題,我們先來看看react事件合成機制。

        二 事件初始化-事件合成,插件機制

        接下來,我們來看一看react這么處理事件合成的。首先我們從上面我們知道,react并不是一次性把所有事件都綁定進去,而是如果發(fā)現(xiàn)項目中有onClick,才綁定click事件,發(fā)現(xiàn)有onChange事件,才綁定blur , change , input , keydown , keyup等。所以為了把原理搞的清清楚楚,筆者把事件原理分成三部分來搞定:

        • 1 react對事件是如何合成的。
        • 2 react事件是怎么綁定的。
        • 3 react事件觸發(fā)流程。

        事件合成-事件插件

        1 必要概念

        我們先來看來幾個常量關(guān)系,這對于我們吃透react事件原理很有幫助。在解析來的講解中,我也會講到這幾個對象如何來的,具體有什么作用。

        ①namesToPlugins

        第一個概念:namesToPlugins 裝事件名 -> 事件模塊插件的映射,namesToPlugins最終的樣子如下:

        const namesToPlugins = {
            SimpleEventPlugin,
            EnterLeaveEventPlugin,
            ChangeEventPlugin,
            SelectEventPlugin,
            BeforeInputEventPlugin,
        }

        SimpleEventPlugin等是處理各個事件函數(shù)的插件,比如一次點擊事件,就會找到SimpleEventPlugin對應(yīng)的處理函數(shù)。我們先記錄下它,至于具體有什么作用,接下來會講到。

        ②plugins

        plugins,這個對象就是上面注冊的所有插件列表,初始化為空。

        const  plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];

        ③registrationNameModules

        registrationNameModules記錄了React合成的事件-對應(yīng)的事件插件的關(guān)系,在React中,處理props中事件的時候,會根據(jù)不同的事件名稱,找到對應(yīng)的事件插件,然后統(tǒng)一綁定在document上。對于沒有出現(xiàn)過的事件,就不會綁定,我們接下來會講到。registrationNameModules大致的樣子如下所示。

        {
            onBlur: SimpleEventPlugin,
            onClick: SimpleEventPlugin,
            onClickCapture: SimpleEventPlugin,
            onChange: ChangeEventPlugin,
            onChangeCapture: ChangeEventPlugin,
            onMouseEnter: EnterLeaveEventPlugin,
            onMouseLeave: EnterLeaveEventPlugin,
            ...
        }

        ④事件插件

        那么我們首先就要搞清楚,SimpleEventPlugin,EnterLeaveEventPlugin每個插件都是什么?我們拿SimpleEventPlugin為例,看一下它究竟是什么樣子?

        const SimpleEventPlugin = {
            eventTypes:{ 
                'click':{ /* 處理點擊事件  */
                    phasedRegistrationNames:{
                        bubbled'onClick',       // 對應(yīng)的事件冒泡 - onClick 
                        captured:'onClickCapture' //對應(yīng)事件捕獲階段 - onClickCapture
                    },
                    dependencies: ['click'], //事件依賴
                    ...
                },
                'blur':{ /* 處理失去焦點事件 */ },
                ...
            }
            extractEvents:function(topLevelType,targetInst,)/* eventTypes 里面的事件對應(yīng)的統(tǒng)一事件處理函數(shù),接下來會重點講到 */ }
        }

        首先事件插件是一個對象,有兩個屬性,第一個extractEvents作為事件統(tǒng)一處理函數(shù),第二個eventTypes是一個對象,對象保存了原生事件名和對應(yīng)的配置項dispatchConfig的映射關(guān)系。由于v16React的事件是統(tǒng)一綁定在document上的,React用獨特的事件名稱比如onClickonClickCapture,來說明我們給綁定的函數(shù)到底是在冒泡事件階段,還是捕獲事件階段執(zhí)行。

        ⑤ registrationNameDependencies

        registrationNameDependencies用來記錄,合成事件比如 onClick 和原生事件 click對應(yīng)關(guān)系。比如 onChange 對應(yīng) change , input , keydown , keyup事件。

        {
            onBlur: ['blur'],
            onClick: ['click'],
            onClickCapture: ['click'],
            onChange: ['blur''change''click''focus''input''keydown''keyup''selectionchange'],
            onMouseEnter: ['mouseout''mouseover'],
            onMouseLeave: ['mouseout''mouseover'],
            ...
        }

        2 事件初始化

        對于事件合成,v16.13.1版本react采用了初始化注冊方式。

        react-dom/src/client/ReactDOMClientInjection.js

        /* 第一步:注冊事件:  */
        injectEventPluginsByName({
            SimpleEventPlugin: SimpleEventPlugin,
            EnterLeaveEventPlugin: EnterLeaveEventPlugin,
            ChangeEventPlugin: ChangeEventPlugin,
            SelectEventPlugin: SelectEventPlugin,
            BeforeInputEventPlugin: BeforeInputEventPlugin,
        });

        injectEventPluginsByName 這個函數(shù)具體有什么用呢,它在react底層是默認執(zhí)行的。我們來簡化這個函數(shù),看它到底是干什么的。

        legacy-event/EventPluginRegistry.js

        /* 注冊事件插件 */
        export function injectEventPluginsByName(injectedNamesToPlugins){
             for (const pluginName in injectedNamesToPlugins) {
                 namesToPlugins[pluginName] = injectedNamesToPlugins[pluginName]
             }
             recomputePluginOrdering()
        }

        injectEventPluginsByName做的事情很簡單,形成上述的namesToPlugins,然后執(zhí)行recomputePluginOrdering,我們接下來看一下recomputePluginOrdering做了寫什么?

        const eventPluginOrder = [ 'SimpleEventPlugin' , 'EnterLeaveEventPlugin','ChangeEventPlugin','SelectEventPlugin' , 'BeforeInputEventPlugin' ]

        function recomputePluginOrdering(){
            for (const pluginName in namesToPlugins) {
                /* 找到對應(yīng)的事件處理插件,比如 SimpleEventPlugin  */
                const pluginModule = namesToPlugins[pluginName];
                const pluginIndex = eventPluginOrder.indexOf(pluginName);
                /* 填充 plugins 數(shù)組  */
                plugins[pluginIndex] = pluginModule;
            }

            const publishedEvents = pluginModule.eventTypes;
            for (const eventName in publishedEvents) {
               // publishedEvents[eventName] -> eventConfig , pluginModule -> 事件插件 , eventName -> 事件名稱
                publishEventForPlugin(publishedEvents[eventName],pluginModule,eventName,)
            } 
        }

        recomputePluginOrdering,作用很明確了,形成上面說的那個plugins,數(shù)組。然后就是重點的函數(shù)publishEventForPlugin。

        /*
          dispatchConfig -> 原生事件對應(yīng)配置項 { phasedRegistrationNames :{  冒泡 捕獲  } ,   }
          pluginModule -> 事件插件 比如SimpleEventPlugin  
          eventName -> 原生事件名稱。
        */

        function publishEventForPlugin (dispatchConfig,pluginModule,eventName){
            eventNameDispatchConfigs[eventName] = dispatchConfig;
            /* 事件 */
            const phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
            if (phasedRegistrationNames) {
            for (const phaseName in phasedRegistrationNames) {
                if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
                    // phasedRegistrationName React事件名 比如 onClick / onClickCapture
                    const phasedRegistrationName = phasedRegistrationNames[phaseName];
                    // 填充形成 registrationNameModules React 合成事件 -> React 處理事件插件映射關(guān)系
                    registrationNameModules[phasedRegistrationName] = pluginModule;
                    // 填充形成 registrationNameDependencies React 合成事件 -> 原生事件 映射關(guān)系
                    registrationNameDependencies[phasedRegistrationName] = pluginModule.eventTypes[eventName].dependencies;
                }
            }
            return true;
            }
        }

        publishEventForPlugin 作用形成上述的 registrationNameModulesregistrationNameDependencies 對象中的映射關(guān)系。

        3 事件合成總結(jié)

        到這里整個初始化階段已經(jīng)完事了,我來總結(jié)一下初始化事件合成都做了些什么。這個階段主要形成了上述的幾個重要對象,構(gòu)建初始化React合成事件和原生事件的對應(yīng)關(guān)系,合成事件和對應(yīng)的事件處理插件關(guān)系。接下來就是事件綁定階段。

        三 事件綁定-從一次點擊事件開始

        事件綁定流程

        如果我們在一個組件中這么寫一個點擊事件,React會一步步如何處理。

        1 diffProperties 處理React合成事件

        <div>
          <button onClick={ this.handerClick }  className="button" >點擊</button>
        </div>

        第一步,首先通過上面的講解,我們綁定給hostComponent種類的fiber(如上的button元素),會 button 對應(yīng)的fiber上,以memoizedPropspendingProps形成保存。

        button 對應(yīng) fiber
        memoizedProps = {
           onClick:function handerClick(){},
           className:'button'
        }

        結(jié)構(gòu)圖如下所示:

        58E6A4AF-1902-42BC-9D11-B47234037E01.jpg

        第二步,React在調(diào)合子節(jié)點后,進入diff階段,如果判斷是HostComponent(dom元素)類型的fiber,會用diff props函數(shù)diffProperties單獨處理。

        react-dom/src/client/ReactDOMComponent.js

        function diffProperties(){
            /* 判斷當(dāng)前的 propKey 是不是 React合成事件 */
            if(registrationNameModules.hasOwnProperty(propKey)){
                 /* 這里多個函數(shù)簡化了,如果是合成事件, 傳入成事件名稱 onClick ,向document注冊事件  */
                 legacyListenToEvent(registrationName, document);
            }
        }

        diffProperties函數(shù)在 diff props 如果發(fā)現(xiàn)是合成事件(onClick) 就會調(diào)用legacyListenToEvent函數(shù)。注冊事件監(jiān)聽器。

        2 legacyListenToEvent 注冊事件監(jiān)聽器

        react-dom/src/events/DOMLegacyEventPluginSystem.js

        //  registrationName -> onClick 事件
        //  mountAt -> document or container
        function legacyListenToEvent(registrationName,mountAt){
           const dependencies = registrationNameDependencies[registrationName]; // 根據(jù) onClick 獲取  onClick 依賴的事件數(shù)組 [ 'click' ]。
            for (let i = 0; i < dependencies.length; i++) {
            const dependency = dependencies[i];
            //這個經(jīng)過多個函數(shù)簡化,如果是 click 基礎(chǔ)事件,會走 legacyTrapBubbledEvent ,而且都是按照冒泡處理
             legacyTrapBubbledEvent(dependency, mountAt);
          }
        }

        legacyTrapBubbledEvent 就是執(zhí)行將綁定真正的dom事件的函數(shù) legacyTrapBubbledEvent(冒泡處理)。

        function legacyTrapBubbledEvent(topLevelType,element){
           addTrappedEventListener(element,topLevelType,PLUGIN_EVENT_SYSTEM,false)
        }

        第三步:在legacyListenToEvent函數(shù)中,先找到 React 合成事件對應(yīng)的原生事件集合,比如 onClick -> ['click'] , onChange -> [blur , change , input , keydown , keyup],然后遍歷依賴項的數(shù)組,綁定事件,這就解釋了,為什么我們在剛開始的demo中,只給元素綁定了一個onChange事件,結(jié)果在document上出現(xiàn)很多事件監(jiān)聽器的原因,就是在這個函數(shù)上處理的。

        我們上面已經(jīng)透露了React是采用事件綁定,React 對于 click 等基礎(chǔ)事件,會默認按照事件冒泡階段的事件處理,不過這也不絕對的,比如一些事件的處理,有些特殊的事件是按照事件捕獲處理的。

        case TOP_SCROLL: {                                // scroll 事件
            legacyTrapCapturedEvent(TOP_SCROLL, mountAt); // legacyTrapCapturedEvent 事件捕獲處理。
            break;
        }
        case TOP_FOCUS: // focus 事件
        case TOP_BLUR:  // blur 事件
        legacyTrapCapturedEvent(TOP_FOCUS, mountAt);
        legacyTrapCapturedEvent(TOP_BLUR, mountAt);
        break;

        3 綁定 dispatchEvent,進行事件監(jiān)聽

        如上述的scroll事件,focus 事件 ,blur事件等,是默認按照事件捕獲邏輯處理。接下來就是最重要關(guān)鍵的一步。React是如何綁定事件到document?事件處理函數(shù)函數(shù)又是什么?問題都指向了上述的addTrappedEventListener,讓我們來揭開它的面紗。

        /*
          targetContainer -> document
          topLevelType ->  click
          capture = false
        */

        function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){
           const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer) 
           if(capture){
               // 事件捕獲階段處理函數(shù)。
           }else{
               /* TODO: 重要, 這里進行真正的事件綁定。*/
              targetContainer.addEventListener(topLevelType,listener,false// document.addEventListener('click',listener,false)
           }
        }

        第四步:這個函數(shù)內(nèi)容雖然不多,但是卻非常重要,首先綁定我們的事件統(tǒng)一處理函數(shù) dispatchEvent,綁定幾個默認參數(shù),事件類型 topLevelType demo中的click ,還有綁定的容器doucment然后真正的事件綁定,添加事件監(jiān)聽器addEventListener。 事件綁定階段完畢。

        4 事件綁定過程總結(jié)

        我們來做一下事件綁定階段的總結(jié)。

        • ① 在React,diff DOM元素類型的fiber的props的時候, 如果發(fā)現(xiàn)是React合成事件,比如onClick,會按照事件系統(tǒng)邏輯單獨處理。
        • ② 根據(jù)React合成事件類型,找到對應(yīng)的原生事件的類型,然后調(diào)用判斷原生事件類型,大部分事件都按照冒泡邏輯處理,少數(shù)事件會按照捕獲邏輯處理(比如scroll事件)。
        • ③ 調(diào)用 addTrappedEventListener 進行真正的事件綁定,綁定在document上,dispatchEvent 為統(tǒng)一的事件處理函數(shù)。
        • 有一點值得注意: 只有上述那幾個特殊事件比如 scorll,focus,blur等是在事件捕獲階段發(fā)生的,其他的都是在事件冒泡階段發(fā)生的,無論是onClick還是onClickCapture都是發(fā)生在冒泡階段,至于 React 本身怎么處理捕獲邏輯的。我們接下來會講到。

        四 事件觸發(fā)-一次點擊事件,在react底層系統(tǒng)會發(fā)生什么?

        <div>
          <button onClick={ this.handerClick }  className="button" >點擊</button>
        </div>

        還是上面這段代碼片段,當(dāng)點擊一下按鈕,在 React 底層會發(fā)生什么呢?接下來,讓我共同探索事件觸發(fā)的奧秘。

        事件觸發(fā)處理函數(shù) dispatchEvent

        我們在事件綁定階段講過,React事件注冊時候,統(tǒng)一的監(jiān)聽器dispatchEvent,也就是當(dāng)我們點擊按鈕之后,首先執(zhí)行的是dispatchEvent函數(shù),因為dispatchEvent前三個參數(shù)已經(jīng)被bind了進去,所以真正的事件源對象event,被默認綁定成第四個參數(shù)。

        react-dom/src/events/ReactDOMEventListener.js

        function dispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
            /* 嘗試調(diào)度事件 */
            const blockedOn = attemptToDispatchEvent( topLevelType,eventSystemFlags, targetContainer, nativeEvent);
        }
        /*
        topLevelType -> click
        eventSystemFlags -> 1
        targetContainer -> document
        nativeEvent -> 原生事件的 event 對象
        */

        function attemptToDispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
            /* 獲取原生事件 e.target */
            const nativeEventTarget = getEventTarget(nativeEvent)
            /* 獲取當(dāng)前事件,最近的dom類型fiber ,我們 demo中 button 按鈕對應(yīng)的 fiber */
            let targetInst = getClosestInstanceFromNode(nativeEventTarget); 
            /* 重要:進入legacy模式的事件處理系統(tǒng) */
            dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst,);
            return null;
        }

        在這個階段主要做了這幾件事:

        • ① 首先根據(jù)真實的事件源對象,找到 e.target 真實的 dom 元素。
        • ② 然后根據(jù)dom元素,找到與它對應(yīng)的 fiber 對象targetInst,在我們 demo 中,找到 button 按鈕對應(yīng)的 fiber。
        • ③ 然后正式進去legacy模式的事件處理系統(tǒng),也就是我們目前用的React模式都是legacy模式下的,在這個模式下,批量更新原理,即將拉開帷幕。

        這里有一點問題,React怎么樣通過原生的dom元素,找到對應(yīng)的fiber的呢? ,也就是說 getClosestInstanceFromNode 原理是什么?

        答案是首先 getClosestInstanceFromNode 可以找到當(dāng)前傳入的 dom 對應(yīng)的最近的元素類型的 fiber 對象。React 在初始化真實 dom 的時候,用一個隨機的 key internalInstanceKey  指針指向了當(dāng)前dom對應(yīng)的fiber對象,fiber對象用stateNode指向了當(dāng)前的dom元素。

        // 聲明隨機key
        var internalInstanceKey = '__reactInternalInstance$' + randomKey;

        // 使用隨機key 
        function getClosestInstanceFromNode(targetNode){
          // targetNode -dom  targetInst -> 與之對應(yīng)的fiber對象
          var targetInst = targetNode[internalInstanceKey];
        }

        在谷歌調(diào)試器上看

        fiber_dom.jpg

        兩者關(guān)系圖

        dom_fiber.jpg

        legacy 事件處理系統(tǒng)與批量更新

        react-dom/src/events/DOMLegacyEventPluginSystem.js

        /* topLevelType - click事件 | eventSystemFlags = 1 | nativeEvent = 事件源對象  | targetInst = 元素對應(yīng)的fiber對象  */
        function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
            /* 從React 事件池中取出一個,將 topLevelType ,targetInst 等屬性賦予給事件  */
            const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
            try { /* 執(zhí)行批量更新 handleTopLevel 為事件處理的主要函數(shù) */
            batchedEventUpdates(handleTopLevel, bookKeeping);
          } finally {
            /* 釋放事件池 */  
            releaseTopLevelCallbackBookKeeping(bookKeeping);
          }
        }

        對于v16事件池,我們接下來會講到,首先 batchedEventUpdates為批量更新的主要函數(shù)。我們先來看看batchedEventUpdates

        react-dom/src/events/ReactDOMUpdateBatching.js

        export function batchedEventUpdates(fn,a){
            isBatchingEventUpdates = true;
            try{
               fn(a) // handleTopLevel(bookKeeping)
            }finally{
                isBatchingEventUpdates = false
            }
        }

        批量更新簡化成如上的樣子,從上面我們可以看到,React通過開關(guān)isBatchingEventUpdates來控制是否啟用批量更新。fn(a),事件上調(diào)用的是 handleTopLevel(bookKeeping) ,由于js是單線程的,我們真正在組件中寫的事件處理函數(shù),比如demo 的 handerClick實際執(zhí)行是在handleTopLevel(bookKeeping)中執(zhí)行的。所以如果我們在handerClick里面觸發(fā)setState,那么就能讀取到isBatchingEventUpdates = true這就是React的合成事件為什么具有批量更新的功能了。比如我們這么寫

        state={number:0}
        handerClick = () =>{
            this.setState({numberthis.state.number + 1   })
            console.log(this.state.number) //0
            this.setState({numberthis.state.number + 1   })
            console.log(this.state.number) //0
            setTimeout(()=>{
                this.setState({numberthis.state.number + 1   })
                console.log(this.state.number) //2
                this.setState({numberthis.state.number + 1   })
                console.log(this.state.number)// 3
            })
        }

        如上述所示,第一個setState和第二個setState在批量更新條件之內(nèi)執(zhí)行,所以打印不會是最新的值,但是如果是發(fā)生在setTimeout中,由于eventLoop 放在了下一次事件循環(huán)中執(zhí)行,此時 batchedEventUpdates 中已經(jīng)執(zhí)行完isBatchingEventUpdates = false,所以批量更新被打破,我們就可以直接訪問到最新變化的值了。

        接下來我們有兩點沒有梳理:

        • 一是React事件池概念
        • 二是最后的線索是執(zhí)行handleTopLevel(bookKeeping),那么handleTopLevel到底做了寫什么。

        執(zhí)行事件插件函數(shù)

        上面說到整個事件系統(tǒng),最后指向函數(shù) handleTopLevel(bookKeeping) 那么 handleTopLevel 到底做了什么事情?

        // 流程簡化后
        // topLevelType - click  
        // targetInst - button Fiber
        // nativeEvent
        function handleTopLevel(bookKeeping){
            const { topLevelType,targetInst,nativeEvent,eventTarget, eventSystemFlags} = bookKeeping
            for(let i=0; i < plugins.length;i++ ){
                const possiblePlugin = plugins[i];
                /* 找到對應(yīng)的事件插件,形成對應(yīng)的合成event,形成事件執(zhí)行隊列  */
                const  extractedEvents = possiblePlugin.extractEvents(topLevelType,targetInst,nativeEvent,eventTarget,eventSystemFlags)  
            }
            if (extractedEvents) {
                events = accumulateInto(events, extractedEvents);
            }
            /* 執(zhí)行事件處理函數(shù) */
            runEventsInBatch(events);
        }

        我把整個流程簡化,只保留了核心的流程,handleTopLevel最后的處理邏輯就是執(zhí)行我們說的事件處理插件(SimpleEventPlugin)中的處理函數(shù)extractEvents,比如我們demo中的點擊事件 onClick 最終走的就是 SimpleEventPlugin 中的 extractEvents 函數(shù),那么React為什么這么做呢? 我們知道我們React是采取事件合成,事件統(tǒng)一綁定,并且我們寫在組件中的事件處理函數(shù)( handerClick ),也不是真正的執(zhí)行函數(shù)dispatchAciton,那么我們在handerClick的事件對象 event,也是React單獨合成處理的,里面單獨封裝了比如 stopPropagationpreventDefault等方法,這樣的好處是,我們不需要跨瀏覽器單獨處理兼容問題,交給React底層統(tǒng)一處理。

        extractEvents 形成事件對象event 和 事件處理函數(shù)隊列

        重點來了!重點來了!重點來了!,extractEvents 可以作為整個事件系統(tǒng)核心函數(shù),我們先回到最初的demo,如果我們這么寫,那么四個回調(diào)函數(shù),那么點擊按鈕,四個事件是如何處理的呢。首先如果點擊按鈕,最終走的就是extractEvents函數(shù),一探究竟這個函數(shù)。

        legacy-events/SyntheticEvent.js

        const  SimpleEventPlugin = {
            extractEvents:function(topLevelType,targetInst,nativeEvent,nativeEventTarget){
                const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
                if (!dispatchConfig) {
                    return null;
                }
                switch(topLevelType){
                    default:
                    EventConstructor = SyntheticEvent;
                    break;
                }
                /* 產(chǎn)生事件源對象 */
                const event = EventConstructor.getPooled(dispatchConfig,targetInst,nativeEvent,nativeEventTarget)
                const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
                const dispatchListeners = [];
                const {bubbled, captured} = phasedRegistrationNames; /* onClick / onClickCapture */
                const dispatchInstances = [];
                /* 從事件源開始逐漸向上,查找dom元素類型HostComponent對應(yīng)的fiber ,收集上面的React合成事件,onClick / onClickCapture  */
                 while (instance !== null) {
                      const {stateNode, tag} = instance;
                      if (tag === HostComponent && stateNode !== null) { /* DOM 元素 */
                           const currentTarget = stateNode;
                           if (captured !== null) { /* 事件捕獲 */
                                /* 在事件捕獲階段,真正的事件處理函數(shù) */
                                const captureListener = getListener(instance, captured);
                                if (captureListener != null) {
                                /* 對應(yīng)發(fā)生在事件捕獲階段的處理函數(shù),邏輯是將執(zhí)行函數(shù)unshift添加到隊列的最前面 */
                                    dispatchListeners.unshift(captureListener);
                                    dispatchInstances.unshift(instance);
                                    dispatchCurrentTargets.unshift(currentTarget);
                                }
                            }
                            if (bubbled !== null) { /* 事件冒泡 */
                                /* 事件冒泡階段,真正的事件處理函數(shù),邏輯是將執(zhí)行函數(shù)push到執(zhí)行隊列的最后面 */
                                const bubbleListener = getListener(instance, bubbled);
                                if (bubbleListener != null) {
                                    dispatchListeners.push(bubbleListener);
                                    dispatchInstances.push(instance);
                                    dispatchCurrentTargets.push(currentTarget);
                                }
                            }
                      }
                      instance = instance.return;
                 }
                  if (dispatchListeners.length > 0) {
                      /* 將函數(shù)執(zhí)行隊列,掛到事件對象event上 */
                    event._dispatchListeners = dispatchListeners;
                    event._dispatchInstances = dispatchInstances;
                    event._dispatchCurrentTargets = dispatchCurrentTargets;
                 }
                return event
            }
        }

        事件插件系統(tǒng)的核心extractEvents主要做的事是:

        • ① 首先形成React事件獨有的合成事件源對象,這個對象,保存了整個事件的信息。將作為參數(shù)傳遞給真正的事件處理函數(shù)(handerClick)。
        • ② 然后聲明事件執(zhí)行隊列 ,按照冒泡捕獲邏輯,從事件源開始逐漸向上,查找dom元素類型HostComponent對應(yīng)的fiber ,收集上面的 React 合成事件,例如 onClick / onClickCapture ,對于冒泡階段的事件(onClick),將 push 到執(zhí)行隊列后面 , 對于捕獲階段的事件(onClickCapture),將 unShift到執(zhí)行隊列的前面。
        • ③ 最后將事件執(zhí)行隊列,保存到React事件源對象上。等待執(zhí)行。

        舉個例子比如如下

        handerClick = () => console.log(1)
        handerClick1 = () => console.log(2)
        handerClick2 = () => console.log(3
        handerClick3= () => console.log(4)
        render(){
            return <div onClick={ this.handerClick2 } onClickCapture={this.handerClick3}  > 
                <button onClick={ this.handerClick }  onClickCapture={ this.handerClick1  }  className="button" >點擊</button>
            </div>

        }

        打印 // 4  2  1  3

        看到這里我們應(yīng)該知道上述函數(shù)打印順序為什么了吧,首先遍歷 button 對應(yīng)的fiber,首先遇到了 onClickCapture ,將 handerClick1  放到了數(shù)組最前面,然后又把onClick對應(yīng)handerClick的放到數(shù)組的最后面,形成的結(jié)構(gòu)是[ handerClick1 , handerClick ] , 然后向上遍歷,遇到了div對應(yīng)fiber,將onClickCapture對應(yīng)的handerClick3放在了數(shù)組前面,將onClick對應(yīng)的 handerClick2 放在了數(shù)組后面,形成的結(jié)構(gòu) [ handerClick3,handerClick1 , handerClick,handerClick2 ] ,所以執(zhí)行的順序 // 4  2  1  3,就是這么簡單,完美!

        FDEBA681-2E03-420B-A838-5907439837A9.jpg

        事件觸發(fā)

        有的同學(xué)可能好奇React的事件源對象是什么樣的,以上面代碼中SyntheticEvent為例子我們一起來看看:

        legacy-events/SyntheticEvent.js/

        function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
          this.dispatchConfig = dispatchConfig;
          this._targetInst = targetInst;
          this.nativeEvent = nativeEvent;
          this._dispatchListeners = null;
          this._dispatchInstances = null;
          this._dispatchCurrentTargets = null;
          this.isPropagationStopped = () => false/* 初始化,返回為false  */

        }
        SyntheticEvent.prototype={
            stopPropagation(){ this.isPropagationStopped = () => true;  }, /* React單獨處理,阻止事件冒泡函數(shù) */
            preventDefault(){ },  /* React單獨處理,阻止事件捕獲函數(shù)  */
            ...
        }

        handerClick 中打印 e :

        B9180401-93FF-4EF0-A2FB-C2FA43B29550.jpg

        既然事件執(zhí)行隊列和事件源對象都形成了,接下來就是最后一步事件觸發(fā)了。上面大家有沒有注意到一個函數(shù)runEventsInBatch,所有事件綁定函數(shù),就是在這里觸發(fā)的。讓我們一起看看。

        legacy-events/EventBatching.js

        function runEventsInBatch(){
            const dispatchListeners = event._dispatchListeners;
            const dispatchInstances = event._dispatchInstances;
            if (Array.isArray(dispatchListeners)) {
            for (let i = 0; i < dispatchListeners.length; i++) {
              if (event.isPropagationStopped()) { /* 判斷是否已經(jīng)阻止事件冒泡 */
                break;
              }
              
              dispatchListeners[i](event)
            }
          }
          /* 執(zhí)行完函數(shù),置空兩字段 */
          event._dispatchListeners = null;
          event._dispatchInstances = null;
        }

        dispatchListeners[i](event)就是執(zhí)行我們的事件處理函數(shù)比如handerClick,從這里我們知道,我們在事件處理函數(shù)中,返回 false ,并不會阻止瀏覽器默認行為

        handerClick(){ //并不能阻止瀏覽器默認行為。
            return false
        }

        應(yīng)該改成這樣:

        handerClick(e){
            e.preventDefault()
        }

        另一方面React對于阻止冒泡,就是通過isPropagationStopped,判斷是否已經(jīng)阻止事件冒泡。如果我們在事件函數(shù)執(zhí)行隊列中,某一會函數(shù)中,調(diào)用e.stopPropagation(),就會賦值給isPropagationStopped=()=>true,當(dāng)再執(zhí)行 e.isPropagationStopped()就會返回 true ,接下來事件處理函數(shù),就不會執(zhí)行了。

        其他概念-事件池

         handerClick = (e) => {
            console.log(e.target) // button 
            setTimeout(()=>{
                console.log(e.target) // null
            },0)
        }

        對于一次點擊事件的處理函數(shù),在正常的函數(shù)執(zhí)行上下文中打印e.target就指向了dom元素,但是在setTimeout中打印卻是null,如果這不是React事件系統(tǒng),兩次打印的應(yīng)該是一樣的,但是為什么兩次打印不一樣呢?因為在React采取了一個事件池的概念,每次我們用的事件源對象,在事件函數(shù)執(zhí)行之后,可以通過releaseTopLevelCallbackBookKeeping等方法將事件源對象釋放到事件池中,這樣的好處每次我們不必再創(chuàng)建事件源對象,可以從事件池中取出一個事件源對象進行復(fù)用,在事件處理函數(shù)執(zhí)行完畢后,會釋放事件源到事件池中,清空屬性,這就是setTimeout中打印為什么是null的原因了。

        事件觸發(fā)總結(jié)

        我把事件觸發(fā)階段做的事總結(jié)一下:

        • ①首先通過統(tǒng)一的事件處理函數(shù) dispatchEvent,進行批量更新batchUpdate。

        • ②然后執(zhí)行事件對應(yīng)的處理插件中的extractEvents,合成事件源對象,每次React會從事件源開始,從上遍歷類型為 hostComponent即 dom類型的fiber,判斷props中是否有當(dāng)前事件比如onClick,最終形成一個事件執(zhí)行隊列,React就是用這個隊列,來模擬事件捕獲->事件源->事件冒泡這一過程。

        • ③最后通過runEventsInBatch執(zhí)行事件隊列,如果發(fā)現(xiàn)阻止冒泡,那么break跳出循環(huán),最后重置事件源,放回到事件池中,完成整個流程。

        evnent_click.jpg

        五 關(guān)于react v17版本的事件系統(tǒng)

        React v17 整體改動不是很大,但是事件系統(tǒng)的改動卻不小,首先上述的很多執(zhí)行函數(shù),在v17版本不復(fù)存在了。我來簡單描述一下v17事件系統(tǒng)的改版。

        1 事件統(tǒng)一綁定container上,ReactDOM.render(app, container);而不是document上,這樣好處是有利于微前端的,微前端一個前端系統(tǒng)中可能有多個應(yīng)用,如果繼續(xù)采取全部綁定在document上,那么可能多應(yīng)用下會出現(xiàn)問題。

        react_17_delegation.png

        2 對齊原生瀏覽器事件

        React 17中終于支持了原生捕獲事件的支持, 對齊了瀏覽器原生標(biāo)準(zhǔn)。同時 onScroll 事件不再進行事件冒泡。onFocusonBlur 使用原生 focusin, focusout 合成。

        3 取消事件池React 17取消事件池復(fù)用,也就解決了上述在setTimeout打印,找不到e.target的問題。

        六 總結(jié)

        本文從事件合成,事件綁定事件觸發(fā)三個方面詳細介紹了React事件系統(tǒng)原理,希望大家能通過這篇文章更加深入了解v16 React 事件系統(tǒng),如果有疑問和不足之處,也希望大家能在評論區(qū)指出。

        最后, 送人玫瑰,手留余香,覺得有收獲的朋友可以給筆者點贊,關(guān)注一波 ,陸續(xù)更新前端超硬核文章。

        提前透漏:接下來會出一部揭秘react調(diào)度系統(tǒng)的文章。感興趣的同學(xué)請關(guān)注公眾號 前端Sharing  第一時間更新前端硬文。

        往期react文章

        React進階系列

        「React進階」 React全部api解讀+基礎(chǔ)實踐大全(夯實基礎(chǔ)萬字總結(jié))

        「react進階」一文吃透react-hooks原理

        「react進階」一文吃透React高階組件(HOC)

        「react進階」年終送給react開發(fā)者的八條優(yōu)化建議

        參考文檔

        • react源碼

        • React 事件系統(tǒng)工作原理


        如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

        1. 點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
        2. 歡迎加我微信「TH0000666」一起交流學(xué)習(xí)...
        3. 關(guān)注公眾號「前端Sharing」,持續(xù)為你推送精選好文。



        瀏覽 98
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        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>
            丰满少妇在线观看网站 | 无码成人午夜电影免费 | 玛丽莲传媒堕落人妻2 | 免费黄色成人网站在线观看 | 操烧逼| 国产精品系列在线播放 | 欧美黄网站 | 福利一区福利二区 | 国产ts一区二区 | 18禁福利网站 |