結合源碼完全理解 react事件機制原理 04 - 事件執行

clipboard.png

前言

這是 react 事件機制的第四節-事件執行,一塊兒研究下在這個過程當中主要通過了哪些關鍵步驟,本文也是react 事件機制的完結篇,但願本文可讓你對 react 事件執行的原理有必定的理解。前端

文章涉及到的源碼是基於 react15.6.1版本,雖然不是最新版本可是也不會影響咱們對 react 事件機制的總體把握和理解。react

回顧

先簡單的回顧下上一文,事件註冊的結果是是把全部的事件回調保存到了一個對象中數組

clipboard.png

那麼在事件觸發的過程當中上面這個對象有什麼用處呢?瀏覽器

其實就是用來查找事件回調。函數

內容大綱

按照個人理解,事件觸發過程總結爲主要下面幾個步驟動畫

1.進入統一的事件分發函數(dispatchEvent)this

2.結合原生事件找到當前節點對應的ReactDOMComponent對象spa

3.進行事件的合成prototype

3.1根據當前事件類型生成指定的合成對象debug

3.2封裝原生事件和冒泡機制

3.3查找當前節點以及他的全部父級

3.4在listenerBank查找事件回調併合成到 event(合成事件結束)

4.批量處理合成事件內的回調事件(事件觸發完成 end)

說再多不如配個圖

clipboard.png

舉個栗子

在說具體的流程前,先看一個栗子,後面的分析也是基於這個栗子

handleFatherClick=(e)=>{
        console.log('father click');
    }

    handleChildClick=(e)=>{
        console.log('child click');
    }

    render(){
        return <div className="box">
                    <div className="father" onClick={this.handleFatherClick}> father
                        <div className="child" onClick={this.handleChildClick}>child </div>
                    </div>
               </div>
    }

看到這個熟悉的代碼,咱們就已經知道了執行結果。

當我點擊 child div 的時候,會同時觸發father的事件。

clipboard.png

一、進入統一的事件分發函數 (dispatchEvent)

當我點擊child div 的時候,這個時候瀏覽器會捕獲到這個事件,而後通過冒泡,事件被冒泡到 document 上,交給統一事件處理函數 dispatchEvent 進行處理。(上一文中咱們已經說過 document 上已經註冊了一個統一的事件處理函數 dispatchEvent)

clipboard.png

二、結合原生事件找到當前節點對應的ReactDOMComponent對象

在原生事件對象內已經保留了對應的ReactDOMComponent實例,應該是在掛載階段就已經保存了

clipboard.png

看下ReactDOMComponent實例的內容

clipboard.png

三、開始進行事件合成

事件的合成,冒泡的處理以及事件回調的查找都是在合成階段完成的。

clipboard.png

3.1 根據當前事件類型找到對應的合成類,而後進行合成對象的生成

//進行事件合成,根據事件類型得到指定的合成類
var SimpleEventPlugin = {
    eventTypes: eventTypes,
    extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
        var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
        //代碼已省略....
        var EventConstructor;

        switch (topLevelType) {
            //代碼已省略....
            case 'topClick'://【這裏有一個不解的地方】 topLevelType = topClick,執行到這裏了,可是這裏沒有作任何操做
                if (nativeEvent.button === 2) {
                    return null;
                }
            //代碼已省略....
            case 'topContextMenu'://而是會執行到這裏,獲取到鼠標合成類
                EventConstructor = SyntheticMouseEvent;
                break;


            case 'topAnimationEnd':
            case 'topAnimationIteration':
            case 'topAnimationStart':
                EventConstructor = SyntheticAnimationEvent;//動畫類合成事件
                break;

            case 'topWheel':
                EventConstructor = SyntheticWheelEvent;//鼠標滾輪類合成事件
                break;

            case 'topCopy':
            case 'topCut':
            case 'topPaste':
                EventConstructor = SyntheticClipboardEvent;
                break;
        }

        var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
        EventPropagators.accumulateTwoPhaseDispatches(event);
        return event;//最終會返回合成的事件對象
    }

3.2 封裝原生事件和冒泡機制

在這一步會把原生事件對象掛到合成對象的自身,同時增長事件的默認行爲處理和冒泡機制

/**
 * 
 * @param {obj} dispatchConfig 一個配置對象 包含當前的事件依賴 ["topClick"],冒泡和捕獲事件對應的名稱 bubbled: "onClick",captured: "onClickCapture"
 * @param {obj} targetInst 組件實例ReactDomComponent
 * @param {obj} nativeEvent 原生事件對象
 * @param {obj} nativeEventTarget  事件源 e.target = div.child
 */
function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {

    this.dispatchConfig = dispatchConfig;
    this._targetInst = targetInst;
    this.nativeEvent = nativeEvent;//將原生對象保存到 this.nativeEvent
    //此處代碼略.....
    var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;

    //處理事件的默認行爲
    if (defaultPrevented) {
        this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
    } else {
        this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
    }


    //處理事件冒泡 ,thatReturnsFalse 默認返回 false,就是不阻止冒泡
    this.isPropagationStopped = emptyFunction.thatReturnsFalse;
    return this;
}

下面是增長的默認行爲和冒泡機制的處理方法,其實就是改變了當前合成對象的屬性值, 調用了方法後屬性值爲 true,就會阻止默認行爲或者冒泡。

來看下代碼

//在合成類原型上增長preventDefault和stopPropagation方法
_assign(SyntheticEvent.prototype, {
    preventDefault: function preventDefault() {
        // ....略

        this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
    },
    stopPropagation: function stopPropagation() {
        //....略

        this.isPropagationStopped = emptyFunction.thatReturnsTrue;
    }
);

看下 emptyFunction 代碼就明白了
clipboard.png

3.3 根據當前節點實例查找他的全部父級實例存入path

/**
 * 
 * @param {obj} inst 當前節點實例
 * @param {function} fn 處理方法
 * @param {obj} arg 合成事件對象
 */
function traverseTwoPhase(inst, fn, arg) {
    var path = [];//存放全部實例 ReactDOMComponent

    while (inst) {
        path.push(inst);
        inst = inst._hostParent;//層級關係
    }

    var i;

    for (i = path.length; i-- > 0;) {
        fn(path[i], 'captured', arg);//處理捕獲 ,反向處理數組
    }

    for (i = 0; i < path.length; i++) {
        fn(path[i], 'bubbled', arg);//處理冒泡,從0開始處理,咱們直接看冒泡
    }
}

看下 path 長啥樣

clipboard.png

3.4 在listenerBank查找事件回調併合成到 event(事件合成結束)

緊接着上面代碼

fn(path[i], 'bubbled', arg);

上面的代碼會調用下面這個方法,在listenerBank中查找到事件回調,並存入合成事件對象。

/**EventPropagators.js
 * 查找事件回調後,把實例和回調保存到合成對象內
 * @param {obj} inst 組件實例
 * @param {string} phase 事件類型
 * @param {obj} event 合成事件對象
 */
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);//把組件實例進行合併返回一個新數組
    }
}

/**
 * EventPropagators.js
 * 中間調用方法 拿到實例的回調方法
 * @param {obj} inst  實例
 * @param {obj} event 合成事件對象
 * @param {string} propagationPhase 名稱,捕獲capture仍是冒泡bubbled
 */
function listenerAtPhase(inst, event, propagationPhase) {
    var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
    return getListener(inst, registrationName);
}

/**EventPluginHub.js
 * 拿到實例的回調方法
 * @param {obj} inst 組件實例
 * @param {string} registrationName Name of listener (e.g. `onClick`).
 * @return {?function} 返回回調方法
 */
getListener: function getListener(inst, registrationName) {
    var bankForRegistrationName = listenerBank[registrationName];

    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
        return null;
    }

    var key = getDictionaryKey(inst);
    return bankForRegistrationName && bankForRegistrationName[key];
}

這裏要高亮一下
clipboard.png

爲何可以查找到的呢?
由於 inst (組件實例)裏有_rootNodeID,因此也就有了對應關係

clipboard.png
到這裏事件合成對象生成完成,全部的事件回調已保存到了合成對象中。

四、 批量處理合成事件對象內的回調方法(事件觸發完成 end)

第3步生成完 合成事件對象後,調用棧回到了咱們起初執行的方法內

clipboard.png

//在這裏執行事件的回調
runEventQueueInBatch(events);

clipboard.png

到下面這一步中間省略了一些代碼,只貼出主要的代碼,

下面方法會循環處理 合成事件內的回調方法,同時判斷是否禁止事件冒泡。

clipboard.png

貼上最後的執行回調方法的代碼

/**
 * 
 * @param {obj} event 合成事件對象
 * @param {boolean} simulated false
 * @param {fn} listener 事件回調
 * @param {obj} inst 組件實例
 */
function executeDispatch(event, simulated, listener, inst) {
    var type = event.type || 'unknown-event';
    event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);

    if (simulated) {//調試環境的值爲 false,按說生產環境是 true 
        //方法的內容請往下看
        ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
    } else {
        //方法的內容請往下看
        ReactErrorUtils.invokeGuardedCallback(type, listener, event);
    }

    event.currentTarget = null;
}

/** ReactErrorUtils.js
 * @param {String} name of the guard to use for logging or debugging
 * @param {Function} func The function to invoke
 * @param {*} a First argument
 * @param {*} b Second argument
 */
var caughtError = null;
function invokeGuardedCallback(name, func, a) {
    try {
        func(a);//直接執行回調方法
    } catch (x) {
        if (caughtError === null) {
            caughtError = x;
        }
    }
}

var ReactErrorUtils = {
    invokeGuardedCallback: invokeGuardedCallback,
    invokeGuardedCallbackWithCatch: invokeGuardedCallback,
    rethrowCaughtError: function rethrowCaughtError() {
        if (caughtError) {
            var error = caughtError;
            caughtError = null;
            throw error;
        }
    }
};

if (process.env.NODE_ENV !== 'production') {//非生產環境會經過自定義事件去觸發回調
    if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {
        var fakeNode = document.createElement('react');

        ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
            var boundFunc = func.bind(null, a);
            var evtType = 'react-' + name;
            fakeNode.addEventListener(evtType, boundFunc, false);
            var evt = document.createEvent('Event');
            evt.initEvent(evtType, false, false);
            fakeNode.dispatchEvent(evt);
            fakeNode.removeEventListener(evtType, boundFunc, false);
        };
    }
}

clipboard.png

最後react 經過生成了一個臨時節點fakeNode,而後爲這個臨時元素綁定事件處理程序,而後建立自定義事件 Event,經過fakeNode.dispatchEvent方法來觸發事件,而且觸發完畢以後當即移除監聽事件。

到這裏事件回調已經執行完成,可是也有些疑問,爲何在非生產環境須要經過自定義事件來執行回調方法。能夠看下上面的代碼在非生產環境對ReactErrorUtils.invokeGuardedCallback 方法進行了重寫。

五、總結

本文主要是從總體流程上介紹了下 react 事件觸發的過程。

主要流程有:

  1. 進入統一的事件分發函數(dispatchEvent)
  2. 結合原生事件找到當前節點對應的ReactDOMComponent對象
  3. 進行事件的合成

3.1 根據當前事件類型生成指定的合成對象

3.2 封裝原生事件和冒泡機制

3.3 查找當前節點以及他的全部父級

3.4 在listenerBank查找事件回調併合成到 event(事件合成結束)

4.批量處理合成事件內的回調事件(事件觸發完成 end)

其中並無深刻到源碼的細節,包括事務處理、合成的細節等,另外梳理過程當中本身也有一些疑惑的地方,對源碼有興趣的小夥兒能夠深刻研究下,固然仍是但願本文可以帶給你一些啓發,若文章有表述不清或有問題的地方歡迎留言交流。

更多精彩內容歡迎關注個人公衆號 - 前端張大胖

圖片描述

相關文章
相關標籤/搜索