【长文慎入】一文吃透 react 事件机制原理

简介: 上个月有幸研究了 react 事件机制这个知识点,并且在公司内部把自己的理解进行了分享。现在趁还算热乎赶紧的整理下来,留住这个长脸的时刻。

上个月有幸研究了 react 事件机制这个知识点,并且在公司内部把自己的理解进行了分享。现在趁还算热乎赶紧的整理下来,留住这个长脸的时刻。

大纲


主要分为4大块儿,主要是结合源码对 react事件机制的原理 进行分析,希望可以让你对 react事件机制有更清晰的认识和理解。

当然肯定会存在一些表述不清或者理解不够标准的地方,还请各位大神、大佬斧正。

01 - 对事件机制的初步理解和验证

02 - 对于合成的理解

03 - 事件注册机制

04 - 事件执行机制

01 02 是理论的废话,也算是我的个人总结,没兴趣的可以直接跳到 03-事件执行机制。

ps: 本文基于 react15.6.1进行分析,虽然不是最新版本但是也不会影响我们对 react 事件机制的整体把握和理解。

对事件机制的初步理解和验证


react事件机制 的表象理解,验证,意义和思考。

表象理解

先回顾下 对react 事件机制基本理解,react自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。

我们都知道react 的所有事件并没有绑定到具体的dom节点上而是绑定在了document 上,然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),所有节点的事件都会在 document 上触发。

既然已经有了对 react事件 的一个基本的认知,那这个认知是否正确呢?我们可以通过简单的方法进行验证。

验证

验证内容:

所有事件均注册到了元素的最顶层-document 上 节点的事件由统一的入口处理 为了方便,直接通过 cli 创建一个项目。

componentDidMount(){ 
        document.getElementById('btn-reactandnative').addEventListener('click', (e) => {
            console.log('原生+react 事件:   原生事件执行');
        });
    }
    handleNativeAndReact = (e) => {
        console.log('原生+react 事件:  当前执行react事件');
    }
    handleClick=(e)=>{
        console.log('button click');
    }
 render(){
        return <div className="pageIndex"><p>react event!!!</p
                <button id="btn-confirm" onClick={this.handleClick}>react 事件</button>
                <button id="btn-reactandnative" onClick={this.handleNativeAndReact}>原生 + react 事件</button>
        </div>
    }

代码中给两个 button绑定了合成事件,单独给 btn#btn-reactandnative绑定了一个原生的事件。

然后看下 chrome 控制台,查看元素上注册的事件。

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

038731a58a36a7d125b5078418354593_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg


经过简单的验证,可以看到所有的事件根据不同的事件类型都绑定在了 document 上,触发函数统一是 dispatchEvent

试想一下

如果一个节点上同时绑定了合成和原生事件,那么禁止冒泡后执行关系是怎样的呢?

其实读到这里答案已经有了。我们现在基于目前的知识去分析下这个关系。

因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 dispatchEvent统一去处理。

* 得出的结论:*

原生事件阻止冒泡肯定会阻止合成事件的触发。

合成事件的阻止冒泡不会影响原生事件。

为什么呢?先回忆下浏览器事件机制



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

浏览器事件的执行需要经过三个阶段,捕获阶段-目标元素阶段-冒泡阶段。

节点上的原生事件的执行是在目标阶段,然而合成事件的执行是在冒泡阶段,所以原生事件会先合成事件执行,然后再往父节点冒泡。

既然原生都阻止冒泡了,那合成还执行个啥嘞。

好,轮到合成的被阻止冒泡了,那原生会执行吗?当然会了。

因为原生的事件先于合成的执行,所以合成事件内阻止的只是合成的事件冒泡。(代码我就不贴了)

所以得出结论:

原生事件(阻止冒泡)会阻止合成事件的执行

合成事件(阻止冒泡)不会阻止原生事件的执行

两者最好不要混合使用,避免出现一些奇怪的问题

意义

react 自己做这么多的意义是什么?


  1. 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次


  2. 统一规范,解决 ie 事件兼容问题,简化事件逻辑


  3. 对开发者友好

思考

既然 react 帮我们做了这么多事儿,那他的背后的机制是什么样的呢?

事件怎么注册的,事件怎么触发的,冒泡机制怎样实现的呢?

请继续往后看......

对于合成的理解


刚听说合成这个词时候,感觉是特别高大上,很有深度,不是很好理解。

当我大概的了解过react事件机制后,略微了解一些皮毛,我觉得合成不单单是事件的合成和处理,从广义上来说还包括:

对原生事件的封装
对某些原生事件的升级和改造
不同浏览器事件兼容的处理

对原生事件的封装

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

上面代码是给一个元素添加 click事件的回调方法,方法中的参数 e,其实不是原生事件对象而是react包装过的对象,同时原生事件对象被放在了属性 e.nativeEvent内。

通过调试,在执行栈里看下这个参数 e包含哪些属性

802c92a764a4b9c5ec204428b8e40097_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

再看下官方说明文档

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

SyntheticEvent是react合成事件的基类,定义了合成事件的基础公共属性和方法。

react会根据当前的事件类型来使用不同的合成事件对象,比如鼠标单机事件 - SyntheticMouseEvent,焦点事件-SyntheticFocusEvent等,但是都是继承自SyntheticEvent。

43606ef8f139f48e763afc0074dee9b0_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

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

4cfbc69489db7148b16969ec3d025ddf_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

对原生事件的升级和改造

对于有些dom元素事件,我们进行事件绑定之后,react并不是只处理你声明的事件类型,还会额外的增加一些其他的事件,帮助我们提升交互的体验。

这里就举一个例子来说明下:

当我们给input声明个onChange事件,看下 react帮我们做了什么?

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

可以看到react不只是注册了一个onchange事件,还注册了很多其他事件。

而这个时候我们向文本框输入内容的时候,是可以实时的得到内容的。

然而原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。

ps: 上面红色箭头中有一个 invalid事件,这个并没有注册到document上,而是在具体的元素上。我的理解是这个是html5新增的一个事件,当输入的数据不符合验证规则的时候自动触发,然而验证规则和配置都要写在当前input元素上,如果注册到document上这个事件就无效了。

浏览器事件的兼容处理

react在给document注册事件的时候也是对兼容性做了处理。

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

上面这个代码就是给document注册事件,内部其实也是做了对 ie浏览器的兼容做了处理。

以上就是我对于react合成这个名词的理解,其实react内部还处理了很多,我只是简单的举了几个栗子,后面开始聊事件注册和事件派发的机制。

事件注册机制


这是 react 事件机制的第三节 - 事件注册,在这里你将了解 react事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源码进行验证和增强理解。

在这里并不会说非常细节的内容,而是把大概的流程和原理性的内容进行介绍,做到对整体流程有个认知和理解。

大致流程

react 事件注册过程其实主要做了2件事:事件注册、事件存储。

a. 事件注册 - 组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange 等,给 document 上添加事件 -addEventListener,并指定统一的事件处理程序 dispatchEvent。

b. 事件存储 - 就是把 react 组件内的所有事件统一的存放到一个对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。

09f44d3defd2bc8d726470fa74b25bd2_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

关键步骤

上面大致说了事件注册需要完成的两个目标,那完成目标的过程需要经过哪些关键处理呢?

首先 react 拿到将要挂载的组件的虚拟 dom(其实就是 react element 对象),然后处理 react dom 的 props ,判断属性内是否有声明为事件的属性,比如 onClick,onChange,这个时候得到事件类型 click,change 和对应的事件处理程序 fn,然后执行后面 3

a. 完成事件注册

b. 将 react dom ,事件类型,处理函数 fn 放入数组存储

c. 组件挂载完成后,处理 b 步骤生成的数组,经过遍历把事件处理函数存储到 listenerBank(一个对象)

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

源码解析

从 jsx 说起

看个最熟悉的代码,也是我们日常的写法

//此处代码省略
    handleFatherClick=()=>{
    }
    handleChildClick=()=>{
    }
    render(){
        return <div className="box">
                    <div className="father" onClick={this.handleFatherClick}>
                        <div className="child" onClick={this.handleChildClick}>child </div>
                    </div>
               </div>
    }

经过 babel 编译后,可以看到最终调用的方法是 react.createElement,z而且声明的事件类型和回调就是个 props


react.createElement执行的结果会返回一个所谓的虚拟 dom (react element object)


处理组件props,拿到事件类型和回调 fn

ReactDOMComponent在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):

7008a456a827b322f9efa2ad5fe2c3ef_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

可以看下 registrationNameModules 的内容,就不细说了,他就是一个内置的常量。

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

事件注册和事件的存储

事件注册

接着上面的代码执行到了这个方法

  1. enqueuePutListener(this, propKey, nextProp, transaction);

在这个方法里会进行事件的注册以及事件的存储,包括冒泡和捕获的处理

3d424ce86c3fc390811d7d58504c0d73_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

根据当前的组件实例获取到最高父级-也就是document,然后执行方法 listenTo - 也是最关键的一个方法,进行事件绑定处理。

源码文件:ReactBrowerEventEmitter.js

0518d14661bea8bbf16b2e8ed60d08d1_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

最后执行 EventListener.listen(冒泡)或者 EventListener.capture(捕获),单看下冒泡的注册,其实就是 addEventListener的第三个参数是 false

3f121c196c9eef09334f3cfcd7c98b20_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

也可以看到注册事件的时候也对 ie 浏览器做了兼容。

上面没有看到 dispatchEvent 的定义,下面可以看到传入 dispatchEvent 方法的代码。

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

到这里事件注册就完事儿了。

事件存储

开始事件的存储,在 react 里所有事件的触发都是通过 dispatchEvent方法统一进行派发的,而不是在注册的时候直接注册声明的回调,来看下如何存储的 。

react 把所有的事件和事件类型以及react 组件进行关联,把这个关系保存在了一个 map里,也就是一个对象里(键值对),然后在事件触发的时候去根据当前的 组件id事件类型查找到对应的 事件fn

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

结合源码:

function enqueuePutListener(inst, registrationName, listener, transaction) {
  var containerInfo = inst._hostContainerInfo;
  var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  listenTo(registrationName, doc);//这个方法上面已说完
  //这里涉及到了事务,事物会在以后的章节再介绍,主要看事件注册
  //下面的代码是将putListener放入数组,当组件挂载完后会依次执行数组的回调。也就是putListener会依次执行
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,//组件实例
    registrationName: registrationName,//事件类型 click
    listener: listener //事件回调 fn
  });
}
function putListener() {
  var listenerToPut = this;
  //放入数组,回调队列
  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}

大致的流程就是执行完 listenTo(事件注册),然后执行 putListener 方法进行事件存储,所有的事件都会存储到一个对象中 - listenerBank,具体由 EventPluginHub进行管理。

//拿到组件唯一标识 id
    var getDictionaryKey = function getDictionaryKey(inst) {
      return '.' + inst._rootNodeID;
    }
   putListener: function putListener(inst, registrationName, listener) {
    //得到组件 id
        var key = getDictionaryKey(inst);
        //得到listenerBank对象中指定事件类型的对象
        var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
        //存储回调 fn
        bankForRegistrationName[key] = listener;
        //....
  }

listenerBank其实就是一个二级 map,这样的结构更方便事件的查找。

这里的组件 id 就是组件的唯一标识,然后和fn进行关联,在触发阶段就可以找到相关的事件回调。

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

看到这个结构是不是很熟悉呢?就是我们平常使用的 object.

到这里大致的流程已经说完,是不是感觉有点明白又不大明白。

没关系,再来个详细的图,重新理解下。

9427c799d20220738c147c7b5113f679_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

事件执行机制

在事件注册阶段,最终所有的事件和事件类型都会保存到 listenerBank中。

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

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

大致流程

事件触发过程总结为主要下面几个步骤:

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

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

3.开始 事件的合成

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

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

3.3 查找当前元素以及他所有父级

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

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

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

38ff135e5e6303eb9504f205352b1238_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


源码解析

dispatchEvent 进行事件分发

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

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

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

查找ReactDOMComponent

结合原生事件找到当前节点对应的 ReactDOMComponent对象,在原生事件对象内已经保留了对应的 ReactDOMComponent实例的引用,应该是在挂载阶段就已经保存了。

14b8eb8533742ea9cbed579670534fef_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

看下ReactDOMComponent实例的内容

8da819760d0a25ac59ef4fe822bb667d_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

事件合成ing

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

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

合成对象的生成

根据当前事件类型找到对应的合成类,然后进行合成对象的生成

//进行事件合成,根据事件类型获得指定的合成类
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;//最终会返回合成的事件对象
    }

封装原生事件和冒泡机制

在这一步会把原生事件对象挂到合成对象的自身,同时增加事件的默认行为处理和冒泡机制

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

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

查找所有父级实例

根据当前节点实例查找他的所有父级实例存入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 长啥样

371826ab27d7000b7857bfac946cee95_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

事件合成结束

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

371826ab27d7000b7857bfac946cee95_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

为什么能够查找到的呢?

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

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

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

批量处理事件合成对象

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

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

4f1edb808d471a8859532c0693a85679_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

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

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

到下面这一步中间省略了一些代码,只贴出主要的代码,下面方法会循环处理 合成事件内的回调方法,同时判断是否禁止事件冒泡。

b91b910f50e3b4f1306d54dfb8b44b45_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);
        };
    }
}


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

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

总结


主要是从整体流程上介绍了下 react事件的原理,其中并没有深入到源码的各个细节,包括事务处理、合成的细节等,另外梳理过程中自己也有一些疑惑的地方,感觉说原理还能比较容易理解一些,但是一结合源码来写就会觉得乱,因为 react代码过于庞大,而且盘根错节,很难抽离,对源码有兴趣的小伙儿可以深入研究下,当然还是希望本文能够带给你一些启发,若文章有表述不清或有问题的地方欢迎留言、 交流、斧正。

最后


文章行文潦草,有碍观瞻,各位大佬受苦了。^_^...

目录
相关文章
|
7月前
|
算法 前端开发 JavaScript
React的diff算法原理
React的diff算法原理
133 0
|
7月前
|
JSON 缓存 前端开发
【React】React原理面试题集锦
本文集合一些React的原理面试题,方便读者以后面试查漏补缺。作者给出自认为可以让面试官满意的简易答案,如果想要了解更深刻,可以点击链接查看对应的详细博文。在此对链接中的博文作者非常感谢🙏。
147 0
|
2月前
|
存储 前端开发 测试技术
React Hooks 的工作原理
【10月更文挑战第1天】
|
4月前
|
前端开发 算法 JavaScript
React原理之Diff算法
【8月更文挑战第24天】
|
4月前
|
前端开发 JavaScript 算法
如何学习react原理
【8月更文挑战第9天】 如何学习react原理
48 6
|
4月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
41 0
|
4月前
|
前端开发 Java UED
瞬间变身高手!JSF 与 Ajax 强强联手,打造极致用户体验的富客户端应用,让你的应用焕然一新!
【8月更文挑战第31天】JavaServer Faces (JSF) 是 Java EE 标准的一部分,常用于构建企业级 Web 应用。传统 JSF 应用采用全页面刷新方式,可能影响用户体验。通过集成 Ajax 技术,可以显著提升应用的响应速度和交互性。本文详细介绍如何在 JSF 应用中使用 Ajax 构建富客户端应用,并通过具体示例展示 Ajax 在 JSF 中的应用。首先,确保安装 JDK 和支持 Java EE 的应用服务器(如 Apache Tomcat 或 WildFly)。
47 0
|
4月前
|
缓存 JavaScript 前端开发
【React生态进阶】React与Redux完美结合:从原理到实践全面解析构建大规模应用的最佳策略与技巧分享!
【8月更文挑战第31天】React 与 Redux 的结合解决了复杂状态管理的问题,提升了应用性能。本文详细介绍了在 React 应用中引入 Redux 的原因、步骤及最佳实践,包括安装配置、状态管理、性能优化等多方面内容,并提供了代码示例,帮助你构建高性能、易维护的大规模应用。
72 0
|
4月前
|
前端开发 JavaScript 中间件
【前端状态管理之道】React Context与Redux大对决:从原理到实践全面解析状态管理框架的选择与比较,帮你找到最适合的解决方案!
【8月更文挑战第31天】本文通过电子商务网站的具体案例,详细比较了React Context与Redux两种状态管理方案的优缺点。React Context作为轻量级API,适合小规模应用和少量状态共享,实现简单快捷。Redux则适用于大型复杂应用,具备严格的状态管理规则和丰富的社区支持,但配置较为繁琐。文章提供了两种方案的具体实现代码,并从适用场景、维护成本及社区支持三方面进行对比分析,帮助开发者根据项目需求选择最佳方案。
75 0
|
6月前
|
前端开发
前端React篇之React setState 调用的原理、React setState 调用之后发生了什么?是同步还是异步?
前端React篇之React setState 调用的原理、React setState 调用之后发生了什么?是同步还是异步?