addeventlistener事件参数_React事件机制原理

初步理解

表象理解

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

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

试想一下

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

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

得出结论

原生事件阻止冒泡肯定会组织合成事件的触发,合成事件的阻止冒泡不会影响原生事件。

原因在于,浏览器的事件执行机制是执行在前,冒泡在后,所以在原生事件中阻止冒泡会阻止合成事件的执行,反之不成立。

综上,两者最好不要一起使用,避免出现一些奇怪的问题。

意义

React 将事件全部统一交给 document 来委托处理的原因是:

  1. 减少内存消耗,提高性能,不需要注册那么多的事件,一种事件只需要在 document 上注册一次即可
  2. 统一规范,用于解决兼容性问题,简化事件逻辑
  3. 对开发者更加友好

对合成的理解

既然我们对 React 的事件机制有了初步的了解,那么可以知道合成事件并不是简单的合成和处理,从广义上还包括:

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

对原生事件的封装

addeventlistener事件参数_React事件机制原理_第1张图片 img

上面的代码是个一个元素添加点击事件的回调函数,方法中的参数 e 其实并不是原生事件中的 event,而是 React 包装过的对象,同时原生事件中的 event 被放在了这个对象的 nativeEvent 字段。

addeventlistener事件参数_React事件机制原理_第2张图片 img

再看下官网文档

addeventlistener事件参数_React事件机制原理_第3张图片 img

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

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

addeventlistener事件参数_React事件机制原理_第4张图片 img addeventlistener事件参数_React事件机制原理_第5张图片 img addeventlistener事件参数_React事件机制原理_第6张图片 img

对原生事件的升级和改造

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

比如说:

当我们给 input 生命一个 onChange 事件,React 帮我们做了很多工作:

addeventlistener事件参数_React事件机制原理_第7张图片 img

可以看到 React 不只是帮助我们注册一个 onchange 事件,还注册了很多其他的事件。

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

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

ps:图中有一个 invalid 事件是注册在当前元素而非在 document 的,可能是因为这个事件是 HTML5 表单属性特有的,需要在输入框输入的时候进行校验,如果是放到 document 上就不会生效了。

浏览器的兼容处理

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

addeventlistener事件参数_React事件机制原理_第8张图片 img

上面这个代码就可以看出,在给 document 注册事件的时候,内部也同时对 IE 浏览器做了兼容处理。

事件注册机制

大致流程

React 事件注册其实主要做了两件事情:

  • 事件注册:组件挂载阶段,根据组件内声明的事件类型:onclick、onchange 等,给 document 添加事件监听,并制定统一的事件处理程序 dispatchEvent;
  • 事件存储:就是把 React 组件内所有事件统一存放到一个对象内,缓存起来,为了在触发事件的时候能够找到对应的方法去执行。
addeventlistener事件参数_React事件机制原理_第9张图片 img

关键步骤

首先 React 拿到将要挂载在组件的虚拟 DOM(React Element 对象),然后处理 React DOM 的 props,判断属性内是否有声明为事件的属性,比如 onClick、onChange 等,这个时候得到事件类型 click、change 等和与之对应的回调函数,然后执行后面三步:

  1. 完成事件注册
  2. 将 React DOM、事件类型、回调函数放入数组存储
  3. 组件挂载完成后,处理步骤2生成的数组,经过遍历把事件回调函数存储到 **listenerBank(一个对象)**中。
addeventlistener事件参数_React事件机制原理_第10张图片 img

源码解析

从 jsx 说起

//...省略

经过 babel 编译之后,我们看到最终调用方法是 React.createElement,而且生命的事件类型和回调函数就是个 props

addeventlistener事件参数_React事件机制原理_第11张图片 img

React.createElement 执行的结果会返回一个所谓的虚拟DOM(React Element Object)。

addeventlistener事件参数_React事件机制原理_第12张图片 img

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

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

addeventlistener事件参数_React事件机制原理_第13张图片 img

可以看下 registrationNameModules 里面的内容,就是一个内置的常量:

addeventlistener事件参数_React事件机制原理_第14张图片 img

事件注册和事件的存储

事件注册

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

declare 

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

addeventlistener事件参数_React事件机制原理_第15张图片 img

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

addeventlistener事件参数_React事件机制原理_第16张图片 img

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

addeventlistener事件参数_React事件机制原理_第17张图片 img

同时我们看到这里也同样对 IE 浏览器做了兼容。

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

addeventlistener事件参数_React事件机制原理_第18张图片 img

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

事件存储

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

React 把所有的事件和事件类型以及 React 组件进行关联,把这个关系保存在一个 Map 里面,然后在事件触发的时候根据当前的组件id 和事件类型找到对应的事件的回调函数。

addeventlistener事件参数_React事件机制原理_第19张图片 img

综合源码:

function enqueuePutListener(inst, registrationName, listener, transaction) {

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

//拿到组件唯一标识 id

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

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

addeventlistener事件参数_React事件机制原理_第20张图片 img

没看错,虽然我一直称呼为 Map,但其实就是一个我们平常使用的 object。

补充一个详细的完整流程图:

addeventlistener事件参数_React事件机制原理_第21张图片 img

事件执行阶段

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

再触发阶段,我们通过这个对象进行事件的查找,然后执行回调函数。

大致流程

  1. 进入统一的事件分发函数(dispatchEvent)
  2. 结合原生事件找到当前节点对应的 ReactDOMComponent 对象
  3. 开始事件的合成
    1. 根据当前事件类型生成指定的合成对象
    2. 封装原生事件和冒泡机制
    3. 查找当前元素以及它所有父级
    4. 在 listenerBank 查找事件回调并合成到 event(合成事件结束)
  4. 批量处理合成事件内的回调函数(事件触发完成)
addeventlistener事件参数_React事件机制原理_第22张图片 img

举个例子

//...省略

当我们点击 child div 的时候,会同时触发 father 的事件

242a486f9dc0dedeaf4c12e39fa4cbb3.png img

源码解析

dispatchEvent 进行事件分发

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

当我点击 child div 的时候,这个时候浏览器会捕获到这个事件,然后经过冒泡,事件会冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。

addeventlistener事件参数_React事件机制原理_第23张图片 img

查找 ReactDOMComponent

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

addeventlistener事件参数_React事件机制原理_第24张图片 img

看下 ReactDOMComponent 实例的内容:

addeventlistener事件参数_React事件机制原理_第25张图片 img

合成事件ing

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

addeventlistener事件参数_React事件机制原理_第26张图片 img

合成对象的生成

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

//进行事件合成,根据事件类型获得指定的合成类

封装原生事件和冒泡机制

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

/**
 *
 * @param {obj} dispatchConfig 一个配置对象 包含当前的事件依赖 ["topClick"],冒泡和捕获事件对应的名称 bubbled: "onClick",captured: "onClickCapture"
 * @param {obj} targetInst 组件实例ReactDomComponent
 * @param {obj} nativeEvent 原生事件对象
 * @param {obj} nativeEventTarget  事件源 e.target = div.child
 */

下面是增加的默认行为和冒泡机制的处理方法,其实就是改变了当前合成对象的属性值,调用了方法后属性值为 true,就会组织默认行为或者冒泡。

//在合成类原型上增加preventDefault和stopPropagation方法

打印一下 emptyFunction 代码

addeventlistener事件参数_React事件机制原理_第27张图片 img

查找所有父级实例

根据当前节点实力查找他的所有父级实例,并存入 path

/**
 *
 * @param {obj} inst 当前节点实例
 * @param {function} fn 处理方法
 * @param {obj} arg 合成事件对象
 */

path 就是一个数组,里面的元素是 ReactDOMComponent

addeventlistener事件参数_React事件机制原理_第28张图片 img

合成事件结束

在 listenerBank 查找事件回调并合成到 event。

紧接着上面的代码

'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];
}
addeventlistener事件参数_React事件机制原理_第29张图片 img

为什么能够查找到的呢?

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

addeventlistener事件参数_React事件机制原理_第30张图片 img

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

批量处理事件合成对象

批量处理合成事件对象内的回调方法。

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

addeventlistener事件参数_React事件机制原理_第31张图片 img
//在这里执行事件的回调
addeventlistener事件参数_React事件机制原理_第32张图片 img

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

addeventlistener事件参数_React事件机制原理_第33张图片 img

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

/**
 *
 * @param {obj} event 合成事件对象
 * @param {boolean} simulated false
 * @param {fn} listener 事件回调
 * @param {obj} inst 组件实例
 */
addeventlistener事件参数_React事件机制原理_第34张图片 img

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

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

你可能感兴趣的:(react,div,onclick叠加,react在线浏览doc)