react 事件机制04-事件触发原理(完结)

简介: 本文继续接上一篇 react 事件机制03-事件注册 来说下 react 事件机制的事件触发过程,一起研究下在这个过程中主要经过了哪些关键步骤,本文也是react 事件机制的完结篇,希望本文可以让你对 react 事件触发的原理有一定的理解。

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


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


60559600daa9365f1df28179e71700ea_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


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

其实就是查找。


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


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

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

3. 进行事件的合成

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

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

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

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

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


说再多不如配个图

267ca5531929abb7ac75afe155c087c4_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


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


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


ff13b95b9365a09ad002b4da9eb3042d_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png



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


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

8514bf4dc1b29fe5b377a02eba61f813_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


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


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


d4ea2bf2a51e5d8da04107833ae50109_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


看下ReactDOMComponent实例的内容


2e60adb54e1d40f3863799a6b54373a4_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg



3、 开始进行事件合成


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


cc9ca90141519491e173707e00b9aa4d_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


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

eeb55b2bd64802a7a27b22d106962dee_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


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


fa601a191d5fa2d441151e52b2e8ae44_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg



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



这里要高亮一下

042d95b8f0bd58d341d694292f7e0fa7_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


为什么能够查找到的呢?

因为 inst (组件实例)里有_rootNodeID,所以也就有了对应关系


48c80ffce5c6d30da18496b2c3913524_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


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


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


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

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

 

c7e24467c18e95f3c07df0c73501de22_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg



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

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


99aa9ad995ad2610e504dd039abafc8d_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


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


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

 

751995e640769f3487ce7f736e1dfab8_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


最后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)


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

目录
相关文章
|
1月前
|
移动开发 前端开发 JavaScript
React 表单与事件
10月更文挑战第10天
40 1
|
22天前
|
前端开发 JavaScript 开发者
React 事件处理机制详解
【10月更文挑战第23天】本文介绍了 React 的事件处理机制,包括事件绑定、事件对象、常见问题及解决方案。通过基础概念和代码示例,详细讲解了如何处理 `this` 绑定、性能优化、阻止默认行为和事件委托等问题,帮助开发者编写高效、可维护的 React 应用程序。
66 4
|
1月前
|
前端开发 JavaScript IDE
React 事件处理
10月更文挑战第8天
21 1
|
2月前
针对react-captcha-code第三方插件不能每次触发深颜色的处理
针对`react-captcha-code`插件生成浅色验证码的问题,通过改造成class组件`MyCaptcha.js`,自定义颜色和验证码生成逻辑,解决了颜色问题。
29 1
针对react-captcha-code第三方插件不能每次触发深颜色的处理
|
1月前
|
存储 前端开发 测试技术
React Hooks 的工作原理
【10月更文挑战第1天】
|
1月前
|
前端开发 JavaScript
一文详解React事件中this指向,面试必备
一文详解React事件中this指向,面试必备
46 0
|
2月前
|
前端开发 JavaScript
react学习(19)事件处理
react学习(19)事件处理
|
2月前
|
前端开发 JavaScript
React的事件与原生事件的执行顺序?
React的事件与原生事件的执行顺序?
|
3月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
37 0
|
3月前
|
前端开发 Java UED
瞬间变身高手!JSF 与 Ajax 强强联手,打造极致用户体验的富客户端应用,让你的应用焕然一新!
【8月更文挑战第31天】JavaServer Faces (JSF) 是 Java EE 标准的一部分,常用于构建企业级 Web 应用。传统 JSF 应用采用全页面刷新方式,可能影响用户体验。通过集成 Ajax 技术,可以显著提升应用的响应速度和交互性。本文详细介绍如何在 JSF 应用中使用 Ajax 构建富客户端应用,并通过具体示例展示 Ajax 在 JSF 中的应用。首先,确保安装 JDK 和支持 Java EE 的应用服务器(如 Apache Tomcat 或 WildFly)。
44 0