乐趣区

结合源码彻底理解-react事件机制原理-04-事件执行

前言

这是 react 事件机制的第四节 - 事件执行,一起研究下在这个过程中主要经过了哪些关键步骤,本文也是 react 事件机制的完结篇,希望本文可以让你对 react 事件执行的原理有一定的理解。

文章涉及到的源码是基于 react15.6.1 版本,虽然不是最新版本但是也不会影响我们对 react 事件机制的整体把握和理解。

回顾

先简单的回顾下上一文,事件注册的结果是是把所有的事件回调保存到了一个对象中

那么在事件触发的过程中上面这个对象有什么用处呢?

其实就是用来查找事件回调。

内容大纲

按照我的理解,事件触发过程总结为主要下面几个步骤

1. 进入统一的事件分发函数 (dispatchEvent)

2. 结合原生事件找到当前节点对应的 ReactDOMComponent 对象

3. 进行事件的合成

3.1 根据当前事件类型生成指定的合成对象

3.2 封装原生事件和冒泡机制

3.3 查找当前节点以及他的所有父级

3.4 在 listenerBank 查找事件回调并合成到 event(合成事件结束)

4. 批量处理合成事件内的回调事件(事件触发完成 end)

说再多不如配个图

举个栗子

在说具体的流程前,先看一个栗子,后面的分析也是基于这个栗子

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 的事件。

1、进入统一的事件分发函数 (dispatchEvent)

当我点击 child div 的时候,这个时候浏览器会捕获到这个事件,然后经过冒泡,事件被冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。(上一文中我们已经说过 document 上已经注册了一个统一的事件处理函数 dispatchEvent)

2、结合原生事件找到当前节点对应的 ReactDOMComponent 对象

在原生事件对象内已经保留了对应的 ReactDOMComponent 实例,应该是在挂载阶段就已经保存了

看下 ReactDOMComponent 实例的内容

3、开始进行事件合成

事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。

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 代码就明白了

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 长啥样

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];
}

这里要高亮一下

为什么能够查找到的呢?
因为 inst(组件实例)里有_rootNodeID,所以也就有了对应关系


到这里事件合成对象生成完成,所有的事件回调已保存到了合成对象中。

4、批量处理合成事件对象内的回调方法(事件触发完成 end)

第 3 步生成完 合成事件对象后,调用栈回到了我们起初执行的方法内


// 在这里执行事件的回调
runEventQueueInBatch(events);

到下面这一步中间省略了一些代码,只贴出主要的代码,

下面方法会循环处理 合成事件内的回调方法,同时判断是否禁止事件冒泡。

贴上最后的执行回调方法的代码

/**
 * 
 * @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);
        };
    }
}

最后 react 通过生成了一个临时节点 fakeNode,然后为这个临时元素绑定事件处理程序,然后创建自定义事件 Event,通过 fakeNode.dispatchEvent 方法来触发事件,并且触发完毕之后立即移除监听事件。

到这里事件回调已经执行完成,但是也有些疑问,为什么在非生产环境需要通过自定义事件来执行回调方法。可以看下上面的代码在非生产环境对 ReactErrorUtils.invokeGuardedCallback 方法进行了重写。

5、总结

本文主要是从整体流程上介绍了下 react 事件触发的过程。

主要流程有:

  1. 进入统一的事件分发函数 (dispatchEvent)
  2. 结合原生事件找到当前节点对应的 ReactDOMComponent 对象
  3. 进行事件的合成

3.1 根据当前事件类型生成指定的合成对象

3.2 封装原生事件和冒泡机制

3.3 查找当前节点以及他的所有父级

3.4 在 listenerBank 查找事件回调并合成到 event(事件合成结束)

4. 批量处理合成事件内的回调事件(事件触发完成 end)

其中并没有深入到源码的细节,包括事务处理、合成的细节等,另外梳理过程中自己也有一些疑惑的地方,对源码有兴趣的小伙儿可以深入研究下,当然还是希望本文能够带给你一些启发,若文章有表述不清或有问题的地方欢迎留言交流。

更多精彩内容欢迎关注我的公众号 – 前端张大胖

退出移动版