360对js事件不响应怎么解决_「React进阶系列」史上最全React事件机制详解

50d9c255c79dc255dd032399746aa8d3.png

框架总览


  • DOM事件流的三个阶段
  • 关于React事件的疑问
  • React事件绑定机制
  • React事件和原生事件有什么区别
  • React事件和原生事件的执行顺序,可以混用吗
  • React事件如何解决跨浏览器兼容
  • React stopPropagation 与 stopImmediatePropagation
  • 从React的事件机制源码看整个流程
  • 基本流程
  • 事件注册
  • 事件触发
  • 总结
  • 站在巨人肩上

DOM事件流的三个阶段

7d4bc344b9e91eb51e95567a566acfbc.png

1、事件捕获阶段

当某个事件触发时,文档根节点最先接受到事件,然后根据DOM树结构向具体绑定事件的元素传递。该阶段为父元素截获事件提供了机会。

事件传递路径为:

window —> document —> boy —> div—> text

2、目标阶段

具体元素已经捕获事件。之后事件开始向根节点冒泡。

3、事件冒泡阶段

该阶段的开始即是事件的开始,根据DOM树结构由具体触发事件的元素向根节点传递。

事件传递路径:

text—> div —> body —> document —> window

使用addEventListener函数在事件流的的不同阶段监听事件。

DOMEle.addEventListener(‘事件名称’,handleFn,Boolean);

此处第三个参数Boolean即代表监听事件的阶段;

为true时,在在捕获阶段监听事件,执行逻辑处理;

为false时,在冒泡阶段监听事件,执行逻辑处理。

edfd9483865231087d81e488110848cf.png

关于React事件的疑问

1.React事件绑定机制

考虑到浏览器的兼容性和性能问题,React 基于 Virtual DOM 实现了一个SyntheticEvent(合成事件)层,我们所定义的事件处理器会接收到一个SyntheticEvent对象的实例。与原生事件直接在元素上注册的方式不同的是,react的合成事件不会直接绑定到目标dom节点上,用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯直到document节点,因此React组件上声明的事件最终绑定到了document 上。用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射,当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做的好处:

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

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

3.对开发者友好

React Event的主要四个文件是 ReactBrowerEventEmitter.js(负责节点绑定的回调函数,该回调函数执行过程中构建合成事件对象,获取组件实例的绑定回调并执行,若有state变更,则重绘组件),ReactEventListener.js(负责事件注册和事件分发), ReactEventEmitter(负责事件的执行),EventPluginHub.js(负责事件的存储)和ReactEventEmitterMixin.js(负责事件的合成)。

2. React事件和原生事件有什么区别

带着问题用以下用代码来展示两者的区别:

  1. 点击button,最后的输出顺序是什么?
  2. B,G 处的type都是啥?
export default class Test extends React.Component {    componentDidMount() {        document.querySelector('#btn').addEventListener('click', (e) => {            console.log('A inner listener')            setTimeout(() => {                console.log('B inner listener timer', e.type)            })        })        document.body.addEventListener('click', (e) => {            console.log('C document listener')        })        window.addEventListener('click', (e) => {            console.log('D window listener')        })    }    outClick(e) {        setTimeout(() => {            console.log('E out timer', e.type)        })        console.log('F out e', e.type)    }    innerClick = (e) => {        console.log('G inner e',e.type)        e.stopPropagation()    }    render() {        return (            
点我
) }}
1. 最后的输出顺序为 A C G B2. B处的type为click,而G处的type为null
响应过程(对应第一问)

我们参照上题,详细说一下事件的响应过程:

由于我们写的几个监听事件addEventListener,都没有给第三个参数,默认值为false,所以在事件捕获阶段,原生的监听事件没有响应,react合成事件只实现了事件冒泡。所以在捕获阶段没有事件响应。

接着到了事件绑定的阶段,button上挂载了原生事件,于是输出"A",setTimeout中的"B"则进入EVENT LOOP。在上一段中,我们提到react的合成事件是挂载到document上,所以“G”没有输出。

之后进入冒泡阶段,到了div上,与上条同理,不会响应outClick,继续向上冒泡。

之后冒泡到了document上,先响应挂载到document的原生事件,输出"c"。之后接着由里向外响应合成事件队列,即输出"G",由于innerClick函数内设置了e.stopPropagation()。所以阻止了冒泡,父元素的事件响应函数没有执行。React合成事件执行e.stopPropagation()不会影响document层级之前的原生事件冒泡。但是会影响document之后的原生事件。所以没有执行body的事件响应函数。之后再处理EVENT LOOP上的事件,输出'B''.

事件池(对应第二问)

在react中,合成事件被调用后,合成事件对象会被重用,所有属性被置为null

event.constructor.release(event);

所以题目中outClick中通过异步方式访问e.type是取不到任何值的,如果需要保留属性,可以调用event.persist()事件,会保留引用。

总结

(1)命名规范不同

React事件的属性名是采用驼峰形式的,事件处理函数是一个函数;

原生事件通过addEventListener给事件添加事件处理函数

(2)React事件只支持事件冒泡。原生事件通过配置第三个参数,true为事件捕获,false为事件冒泡

(3)事件挂载目标不同

React事件统一挂载到document上;

原生事件挂载到具体的DOM上

(4)this指向不同

原生事件:

1.如果onevent事件属性定义的时候将this作为参数,在函数中获取到该参数是DOM对象。用该方法可以获取当前DOM。

2在方法中直接访问this, this指向当前函数所在的作用域。或者说调用函数的对象。

React事件:

React中this指向一般都期望指向当前组件,如果不绑定this,this一般等于undefined。

React事件需要手动为其绑定this具体原因可以参考文章: 为什么需要在 React 类组件中为事件处理程序绑定 this

(5)事件对象不同

原生js中事件对象是原生事件对象,它存在浏览器兼容性,需要用户自己处理各浏览器兼容问题;

ReactJS中的事件对象是React将原生事件对象(event)进行了跨浏览器包装过的合成事件(SyntheticEvent)。

为了性能考虑,执行完后,合成事件的事件属性将不能再访问

React事件和原生事件的执行顺序,可以混用吗

由上面的代码我们可以理解:

react的所有事件都挂载在document中

当真实dom触发后冒泡到document后才会对react事件进行处理

所以原生的事件会先执行

然后执行react合成事件

最后执行真正在document上挂载的事件

不要将合成事件与原生事件混用。执行React事对象件的e.stopPropagation()可以阻止React事件冒泡。但是不能阻止原生事件冒泡;反之,在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。因为无法将事件冒泡到document上导致的

React事件如何解决跨浏览器兼容

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

7d42f98f8b8932380952379e20878ab0.png

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

其实react内部还处理了很多,比如react合成事件:

React 根据 W3C 规范 定义了这个合成事件,所以你不需要担心跨浏览器的兼容性问题。

事件处理程序将传递 SyntheticEvent 的实例,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括stopPropagation() 和 preventDefault() ,在所有浏览器中他们工作方式都相同。

每个SyntheticEvent对象都具有以下属性:

| 属性名 | 类型 | 描述 |

| ---- | ---- | ---- |

| bubbles | boolean | 事件是否可冒泡|

| cancelable| boolean | 事件是否可拥有取消的默认动作|

| currentTarget | DOMEventTarget| 事件监听器触发该事件的元素(绑定事件的元素)|

| defaultPrevented | boolean| 当前事件是否调用了 event.preventDefault()方法|

| eventPhase| number | 事件传播的所处阶段[0:Event.NONE-没有事件被处理,1:Event.CAPTURING_PHASE - 捕获阶段,2:被目标元素处理,3:冒泡阶段(Event.bubbles为true时才会发生)]|

| isTrusted| boolean| 触发是否来自于用户行为,false为脚本触发|

| nativeEvent | DOMEvent| 浏览器原生事件|

| preventDefault()| void | 阻止事件的默认行为|

| isDefaultPrevented() | boolean | 返回的事件对象上是否调用了preventDefault()方法|

| stopPropagation()| void| 阻止冒泡|

| isPropagationStopped() | boolean | 返回的事件对象上是否调用了stopPropagation()方法|

| target | DOMEventTarget| 触发事件的元素|

| timeStamp | number| 事件生成的日期和时间|

| type | string | 当前 Event 对象表示的事件的名称,是注册事件的句柄,如,click、mouseover…etc.|

React合成的SyntheticEvent采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。

另外,不管在什么浏览器环境下,浏览器会将该事件类型统一创建为合成事件,从而达到了浏览器兼容的目的。

React stopPropagation 与 stopImmediatePropagation

React 合成事件与原生事件执行顺序图:

d564a637c001604eb89b0f54d4d77c72.png

从图中我们可以得到一下结论:

(1)DOM 事件冒泡到document上才会触发React的合成事件,所以React 合成事件对象的e.stopPropagation,只能阻止 React 模拟的事件冒泡,并不能阻止真实的 DOM 事件冒泡

(2)DOM 事件的阻止冒泡也可以阻止合成事件原因是DOM 事件的阻止冒泡使事件不会传播到document上

(3)当合成事件和DOM 事件 都绑定在document上的时候,React的处理是合成事件应该是先放进去的所以会先触发,在这种情况下,原生事件对象的 stopImmediatePropagation能做到阻止进一步触发document DOM事件

stopImmediatePropagation :如果有多个相同类型事件的事件监听函数绑定到同一个元素,则当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation()方法,则剩下的监听函数将不会被执行。


从React的事件机制源码看整个流程

93c514071ae3babefd7fcd0300c4b9d4.png

基本流程

在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:

/** * Summary of `ReactBrowserEventEmitter` event handling: * *  - Top-level delegation is used to ...... * ...... * * +------------+    . * |    DOM     |    . * +------------+    . *       |           . *       v           . * +------------+    . * | ReactEvent |    . * |  Listener  |    . * +------------+    .                         +-----------+ *       |           .               +--------+|SimpleEvent| *       |           .               |         |Plugin     | * +-----|------+    .               v         +-----------+ * |     |      |    .    +--------------+                    +------------+ * |     +-----------.--->|EventPluginHub|                    |    Event   | * |            |    .    |              |     +-----------+  | Propagators| * | ReactEvent |    .    |              |     |TapEvent   |  |------------| * |  Emitter   |    .    |              ||              |                    +------------+ * |     |      |    .    +--------------+ * +-----|------+    .                ^        +-----------+ *       |           .                |        |Enter/Leave| *       +           .                +-------+|Plugin     | * +-------------+   .                         +-----------+ * | application |   . * |-------------|   . * |             |   . * |             |   . * +-------------+   . *                   . *    React Core     .  General Purpose Event Plugin System */

这段注释是在大概描述 React的事件机制,也就是这个文件中的代码要做的一些事情,大概意思就是说事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁。

React内部事件系统实现可以分为两个阶段:事件注册、事件触发,涉及的主要类如下:

ReactEventListener:负责事件注册和事件分发。React将DOM事件全都注册到document节点上,事件分发主要调用dispatchEvent进行,从事件触发组件开始,向父元素遍历。

ReactEventEmitter:负责每个组件上事件的执行。

EventPluginHub:负责回调函数的存储

JSX中声明一个React事件,比如:

render() {  return (    点击  )}

用户点击button按钮触发click事件后,DOM将event传给ReactEventListener,触发document上注册的事件处理函数,执行ReactEventListener.dispatchEvent(event)将事件分发到当前组件及以上的父组件。然后ReactEventEmitter对每个组件进行事件的执行,先构造React合成事件,然后以队列的方式调用JSX中声明的callback。

4451a9e96e1df3abdc9829d6466310d3.png

备注:以下代码逻辑大部分写在注释里面

事件注册

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

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

大致流程

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

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

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

88dc3775c82af1a4d2254abe09d38aee.png
关键步骤

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

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

a. 完成事件注册

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

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

66feba52bc452829401854ac36a726f8.png
源码解析
d0d83935c25d2f507c19451f9bfae446.png

1.从 jsx 说起

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

    //此处代码省略    handleFatherClick=()=>{    }    handleChildClick=()=>{    }    render(){        return 
child
}

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

react.createElement('div',  {    className:'box',  },  react.createElement('div',    {      className:'father',      onClick: this.handleFatherClick    },    react.createElement('div',      {        className:'child',        onClick: this.handleChildClick,      },      'child'    );  ););

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

e75359489b8c614faa4adabaa86d3b10.png

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

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

_updateDOMProperties: function (lastProps, nextProps, transaction) {    ... // 前面代码太长,省略一部分    else if (registrationNameModules.hasOwnProperty(propKey)) {        // 如果是props这个对象直接声明的属性,而不是从原型链中继承而来的,则处理它        // nextProp表示要创建或者更新的属性,而lastProp则表示上一次的属性        // 对于mountComponent,lastProp为null。updateComponent二者都不为null。unmountComponent则nextProp为null        if (nextProp) {          // mountComponent和updateComponent中,enqueuePutListener注册事件          enqueuePutListener(this, propKey, nextProp, transaction);        } else if (lastProp) {          // unmountComponent中,删除注册的listener,防止内存泄漏          deleteListener(this, propKey);        }    }}

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

b779ac811272eaa71f678214bb4322b6.png
事件注册和事件的存储
1.事件注册

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

enqueuePutListener(this, propKey, nextProp, transaction);

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

// inst: React Component对象// registrationName: React合成事件名,如onClick// listener: React事件回调方法,如onClick=callback中的callback// transaction: mountComponent或updateComponent所处的事务流中,React都是基于事务流的function enqueuePutListener(inst, registrationName, listener, transaction) {  if (transaction instanceof ReactServerRenderingTransaction) {    return;  }  var containerInfo = inst._hostContainerInfo;  var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;  // 找到document  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;  // 注册事件,将事件注册到document上  listenTo(registrationName, doc);  // 存储事件,放入事务队列中  transaction.getReactMountReady().enqueue(putListener, {    inst: inst,    registrationName: registrationName,    listener: listener  });}

可以看到这个函数一共做了三件事:

1.根据当前的组件实例获取到最高父级-也就是document;

2.然后执行方法 listenTo - 也是最关键的一个方法,进行事件注册。

  1. 最后执行transaction.getReactMountReady().enqueue,将react dom 实例,事件类型,处理函数 fn 组成一个对象放入数组存储。等待组件挂载后依次为数组里面每一项执行putListener。为数组每一项生成一个映射关系,把这个关系保存在了一个 map里,也就是一个对象(键值对),然后在事件触发的时候去根据当前的组件id和事件类型查找到对应的事件fn。 从ReactBrowserEventEmitter.listenTo;在ReactBrowserEventEmitter

.js文件下找到listenTo方法,可以发现它主要解决了不同浏览器间捕获和冒泡不兼容的问题。click,mousewheel等事件调用trapBubbledEvent来注册冒泡事件;scroll,focus等事件调用trapCapturedEvent来注册捕获事件。

listenTo : function (registrationName, contentDocumentHandle) {    var mountAt = contentDocumentHandle;    var isListening = getListeningForDocument(mountAt);    var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];    for (var i = 0; i < dependencies.length; i++) {      var dependency = dependencies[i];      if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {        if (dependency === 'topWheel') {          if (isEventSupported('wheel')) {            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'wheel', mountAt);          } else if (isEventSupported('mousewheel')) {            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'mousewheel', mountAt);          } else {            // Firefox needs to capture a different mouse scroll event.            // @see http://www.quirksmode.org/dom/events/tests/scroll.html            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'DOMMouseScroll', mountAt);          }        } else if (dependency === 'topScroll') {          if (isEventSupported('scroll', true)) {            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topScroll', 'scroll', mountAt);          } else {            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topScroll', 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);          }        } else if (dependency === 'topFocus' || dependency === 'topBlur') {          if (isEventSupported('focus', true)) {            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);          } else if (isEventSupported('focusin')) {            // IE has `focusin` and `focusout` events which bubble.            // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topFocus', 'focusin', mountAt);            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topBlur', 'focusout', mountAt);          }          // to make sure blur and focus event listeners are only attached once          isListening.topBlur = true;          isListening.topFocus = true;        } else if (topEventMapping.hasOwnProperty(dependency)) {          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);        }        isListening[dependency] = true;      }    }  }

最后执行EventListener.listen(冒泡)或者EventListener.capture(捕获),来看看将事件绑定到冒泡阶段的具体代码:

// 三个参数为 topEvent、原生 DOM Event、Document(挂载节点)trapBubbledEvent: function (topLevelType, handlerBaseName, element) {    if (!element) {        return null;    }    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));}// 三个参数为 Document(挂载节点)、原生 DOM Event、事件绑定函数listen: function listen(target, eventType, callback) {  if (target.addEventListener) {    target.addEventListener(eventType, callback, false);    // 返回一个解绑的函数    return {        remove: function remove() {            target.removeEventListener(eventType, callback, false);        }    }  }  if (target.attachEvent) {    target.attachEvent('on' + eventType, callback);    // 返回一个解绑的函数    return {        remove: function remove() {            target.detachEvent('on' + eventType, callback);        }    }  }}

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

上面没有看到 dispatchEvent 的定义,其实上面代码中的callback统一为dispatchEvent。dispatchEvent将在之后讲。

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

事件存储

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

2ec548a6b7d32454f8c0c5c83613abcf.png

还是上面的源码:

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

大致的流程就是执行完listenTo(事件注册),执行transaction.getReactMountReady().enqueue,将react dom 实例,事件类型,处理函数 fn 组成一个对象放入数组存储。等待组件挂载后依次为数组里面每一项执行putListener。为数组每一项生成一个映射关系,把这个关系保存在了一个 对象(键值对)里,这个对象叫做 listenerBank,如下图。然后在事件触发的时候去根据当前的组件id和事件类型查找到对应的事件fn。

24ae0c68baa2f0fc9276a6917f8d489c.png

事件存储由EventPluginHub来负责,EventPluginHub在react事件系统的核心文件renderers/shared/event/EventPluginHub.js中定义,感兴趣的同学可以去看看源码~~

var EventPluginHub = { injection, putListener, getListener, deleteListener, deleteAllListeners, extractEvents, // 当顶层事件被触发,该方法中会传入原生事件,生成合成事件 enqueueEvents,// 合成事件进入事件队列 processEventQueue, // 调度事件队列上的所有合成事件}

事件存储的入口在我们上面讲到的putListener方法,如下

function putListener() {  var listenerToPut = this;  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);}

实际调用的是EventPluginHub.js中的putListener方法,该方法在组件挂载时执行。EventPluginHub.js主要负责事件的存储、合成事件以对象池的方式实现创建和销毁。

/**   * EventPluginHub用来存储React事件, 将listener存储到`listenerBank[registrationName][key]`   *   * @param {object} inst: 事件源   * @param {string} listener的名字,比如onClick   * @param {function} listener的callback   */  //  var listenerBank = {};  putListener: function (inst, registrationName, listener) {    // 用来标识注册了事件,比如onClick的React对象。key的格式为'.nodeId', 只用知道它可以标示哪个React对象就可以了    var key = getDictionaryKey(inst);    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});    // 将listener事件回调方法存入listenerBank[registrationName][key]中,比如listenerBank['onclick'][nodeId]    // 所有React组件对象定义的所有React事件都会存储在listenerBank中    bankForRegistrationName[key] = listener;    //onSelect和onClick注册了两个事件回调插件, 用于walkAround某些浏览器兼容bug,不用care    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];    if (PluginModule && PluginModule.didPutListener) {      PluginModule.didPutListener(inst, registrationName, listener);    }  },var getDictionaryKey = function (inst) {  return '.' + inst._rootNodeID;};

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

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

c0a6a269485959cccd139ce273f5f394.png

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

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

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

a9460f0c566d6ace3f3f3e8d7f9c8d84.png

事件触发

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

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

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

事件触发过程总结为主要分为3个步骤:事件分发、生成合成事件、批量执行事件回调

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

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

3.开始事件的合成

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

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

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

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

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

115e359cdbefd4c1e9723cd4e8e3d348.png
3de858ea2d86637323a1d3ba6f1e8588.png

举个栗子

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

handleFatherClick=(e)=>{        console.log('father click');    }    handleChildClick=(e)=>{        console.log('child click');    }    render(){        return 
father
child
}

看到这个熟悉的代码,我们就已经知道了执行结果。

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

5954c0e63a8fc385e4342ff06f405d41.png
1. 事件分发
437ad3433dc881a8f9e0fb6f1412dbf5.png

当事件触发时,注册在document上的回调函数会被触发。事件触发的入口函数是ReactEventListener.dispatchEvent负责分发已经注册的回调函数。在这个函数中会调用batchingStrategy 的 batchUpdate 方法实现批量处理更新。batchUpdate以transaction形式调用,批量处理更新。

// topLevelType:带top的事件名,如topClick。不用纠结为什么带一个top字段,知道它是事件名就OK了// nativeEvent: 用户触发click等事件时,浏览器传递的原生事件dispatchEvent: function (topLevelType, nativeEvent) {    // disable了则直接不回调相关方法    if (!ReactEventListener._enabled) {      return;    }     // bookKeeping的初始化使用了react在源码中用到的对象池的方法来避免多余的垃圾回收。     // bookKeeping的作用看ta的定义就知道了,就是一个用来保存过程中会使用到的变量的对象。    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);    try {      // 放入批处理队列中,React事件流也是一个消息队列的方式      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);    } finally {      TopLevelCallbackBookKeeping.release(bookKeeping);    }}

TopLevelCallbackBookKeeping是一个类,该类对象用于记录topLevelType,nativeEvent和用于存储所有的祖先节点数组ancestors(当前是空的,只有分发时才会遍历并存储所有的祖先节点) 。

那么传入batchedUpdates 内部的回调函数handleTopLevelImpl是什么呢???它其实就是事件分发的核心部分。

// document进行事件分发,这样具体的React组件才能得到响应。因为DOM事件是绑定到document上的function handleTopLevelImpl(bookKeeping) {  // 获取发生原生的事件的e.target  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);  // // 获取原生事件的target说在的组件,它是虚拟DOM  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);  // 执行事件回调前,先由当前组件向上遍历它的所有父组件。得到ancestors这个数组。  // 因为事件回调中可能会改变Virtual DOM结构,所以要先遍历好组件层级  var ancestor = targetInst;  do {    bookKeeping.ancestors.push(ancestor);      // 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,    // 我们知道一般情况下,我们的组件最后会被包裹在

从上面的事件分发中可见,React自身实现了一套冒泡机制。从触发事件的对象开始,向父元素回溯,依次调用它们注册的事件callback。

看下ReactDOMComponent实例的内容

d6c46f4cbd60caca4e5824e6facac7c3.png

事件处理由_handleTopLevel完成。它其实是调用ReactBrowserEventEmitter.handleTopLevel() ,如下

// React事件调用的入口。DOM事件绑定在了document原生对象上,每次事件触发,都会调用到handleTopLevel  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {    // 采用对象池的方式构造出合成事件。不同的eventType的合成事件可能不同    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);    // 批处理队列中的events    runEventQueueInBatch(events);  }

handleTopLevel方法是事件callback调用的核心。它主要做两件事情,一方面利用浏览器回传的原生事件构造出React合成事件,另一方面采用队列的方式处理events。先看如何构造合成事件。


2. 事件合成

a018ec051240fb9d0cd160f42a214fd3.png

合成事件时一个跨浏览器原生事件包装器,具有与浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,除了事件在所有浏览器中他们工作方式都相同。现在来看一看React是如何实现合成事件的。

EventPluginHub.js中extractEvents方法:

  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {    var events;    var plugins = EventPluginRegistry.plugins;    for (var i = 0; i < plugins.length; i++) {      // Not every plugin in the ordering may be loaded at runtime.      var possiblePlugin = plugins[i];      if (possiblePlugin) {        var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);        if (extractedEvents) {          events = accumulateInto(events, extractedEvents);        }      }    }    return events;  }
插件中的extractevents

注意不要将EventPluginHub.extractevents和possiblePlugin.extractEvents搞混了

以点击事件click的生成插件SimpleEventPlugin为例:

//进行事件合成,根据事件类型获得指定的合成类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;        }        // 合成事件对象都是以pool方式创建和销毁的,这提高了React的性能,同时也意味着一旦事件执行结束         // 该合成事件对象会被销毁。因此不能通过异步方式获取该事件        var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);        EventPropagators.accumulateTwoPhaseDispatches(event);        return event;//最终会返回合成的事件对象    }

EventPluginHub.extractEvents()方法是通过执行各个插件的extractEvents方法来创建合成事件。possiblePlugin.extractEvents()根据事件的Type创建不同插件的合成事件,accumulateInto()负责将所有插件的合成事件存入到events数组中,形成当前事件的合成事件集合。EventPluginRegistry.plugins默认包含五种plugin,他们是在EventPluginHub初始化阶段注入进去的,分别是 SimpleEventPlugin、EnterLeaveEventPlugin、ChangeEventPlugin、SelectEventPlugin和BeforeInputEventPlugin。根据不同的事件类型采用不同的事件合成方法,这些事件合成方法有SyntheticAnimationEvent.js、SyntheticFocusEvent.js、SyntheticKeyboardEvent.js、SyntheticMouseEvent.js、SyntheticTouchEvent.js、SyntheticUIEvent.js、SyntheticWheelEvent.js等等一共13种合成方法。

上面提到调用EventPropagators.accumulateTwoPhaseDispatches(event)从EventPluginHub中获取回调函数,存储到合成事件的_dispatchListeners属性中。如下:

// EventPropagators.jsfunction accumulateTwoPhaseDispatches(events) {  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);}// forEachAccumulated 函数在接下来会讲到function accumulateTwoPhaseDispatchesSingle(event) {  if (event && event.dispatchConfig.phasedRegistrationNames) {    EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);  }}
把所有父级元素绑定的相关事件按照捕获->冒泡的顺序存push到合成事件对象的_dispatchListeners属性中。该属性为一个数组。
/** *  * @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 长啥样

930acc801c262ab87873da3431648372.png

紧接着如何在listenerBank中查找事件回调并合成到合成对象的_dispatchListeners中呢。

紧接着上面代码

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,所以也就有了对应关系。比如通过上面函数getDictionaryKey获取到触发事件的DOM组件的_rootNodeId属性,然后根据callback = listenerBank[eventType][_rootNodeId]可以获取该组件的回调函数。

86b5eef6929aa76c2bd03bd9ec709a74.png

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

3.批量执行事件回调

事件合成之后就需要执行事件回调函数,React以批量处理事件队列的方式执行事件的。它的入口函数是在ReactEventEmitterMixin.js中的runEventQueueInBatch方法:

var eventQueue = null;function runEventQueueInBatch(events) {  EventPluginHub.enqueueEvents(events);  EventPluginHub.processEventQueue(false);}//EventPluginHub.enqueueEvents()方法是将合成事件写入队列,EventPluginHub.processEventQueue()方法是执行队列中的事件。// EventPluginHub.enqueueEvents()方法的实现逻辑很简单,使用accumulateInto方法将events存入eventQueue队列中。 enqueueEvents: function (events) {    if (events) {      eventQueue = accumulateInto(eventQueue, events);    }  }function accumulateInto(current, next) {  if (current == null) {    return next;  }  // 将next添加到current中,返回一个包含他们两个的新数组  // 如果next是数组,current不是数组,采用push方法,否则采用concat方法  // 如果next不是数组,则返回一个current和next构成的新数组  if (Array.isArray(current)) {    if (Array.isArray(next)) {      current.push.apply(current, next);      return current;    }    current.push(next);    return current;  }  if (Array.isArray(next)) {    return [current].concat(next);  }  return [current, next];}// EventPluginHub.processEventQueue()方法的实现逻辑分为:processEventQueue: function (simulated) {// 现将eventQueue 设置为null,以便在处理过程中让新合成事件入队列    var processingEventQueue = eventQueue;    eventQueue = null;    if (simulated) {      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);    } else {      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);    }       // This would be a good time to rethrow if any of the event handlers threw.    ReactErrorUtils.rethrowCaughtError();  }

(1)获取合成事件队列,其中可能包含之前没处理完的合成事件

(2)遍历合成事件队列,

function forEachAccumulated(arr, cb, scope) {  if (Array.isArray(arr)) {    arr.forEach(cb, scope);  } else if (arr) {    cb.call(scope, arr);  }}

事件队列的回调函数为executeDispatchesAndReleaseSimulated,负责事件的分发和事件遍历结束后是否释放合成事件对象。

var executeDispatchesAndReleaseSimulated = function (e) {  return executeDispatchesAndRelease(e, true);};/**/var executeDispatchesAndRelease = function (event, simulated) {  if (event) {/*按存储顺序分发事件,先进先出*/    EventPluginUtils.executeDispatchesInOrder(event, simulated);/*判断是否释放事件*/    if (!event.isPersistent()) {      event.constructor.release(event);    }  }};var executeDispatchesAndReleaseTopLevel = function (e) {  return executeDispatchesAndRelease(e, false);};var executeDispatchesAndRelease = function (event, simulated) {  if (event) {    //进行事件分发    EventPluginUtils.executeDispatchesInOrder(event, simulated);    if (!event.isPersistent()) {       // 处理完,则release掉event对象,采用对象池方式,减少GC      // React帮我们处理了合成事件的回收机制,不需要我们关心。但要注意,如果使用了DOM原生事件,则要自己回收      event.constructor.release(event);    }  }};// EventPluginUtils.js// 事件处理的核心function executeDispatchesInOrder(event, simulated) {  var dispatchListeners = event._dispatchListeners;  var dispatchInstances = event._dispatchInstances; if (Array.isArray(dispatchListeners)) {    // 如果有多个listener,则遍历执行数组中event    for (var i = 0; i < dispatchListeners.length; i++) {      // 如果isPropagationStopped设成true了,则停止事件传播,退出循环。      if (event.isPropagationStopped()) {        break;      }      // 执行event的分发,从当前触发事件元素向父元素遍历      // event为浏览器上传的原生事件      // dispatchListeners[i]为JSX中声明的事件callback      // dispatchInstances[i]为对应的React Component       executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);    }  } else if (dispatchListeners) {    // 如果只有一个listener,则直接执行事件分发    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);  }  // 处理完event,重置变量。因为使用的对象池,故必须重置,这样才能被别人复用  event._dispatchListeners = null;  event._dispatchInstances = null;}

通过executeDispatchesInOrder函数可知,dispatch 合成事件分为两个步骤:

  • 通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素
  • 循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理

当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡

目前还是还有看到执行事件的代码,在接着看:

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

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


总结

React的事件系统主要分为3个步骤:

一是事件绑定,ReactBrowserEventEmitter的trapBubbledEvent等方法为节点或文档绑定事件;

二是事件监听,ReactEventListener.dispatchEvent将把该回调函数分发给事件对象的_dispatchListener属性;调用ReactBrowserEventEmitter.ReactEventListener方法以监听节点事件。

三是事件分发与触发,对触发的事件进行分发,并创建合成事件对象,在回调中用构建合成事件对象并执行合成事件对的象绑定回调。

站在巨人肩上

一看就晕的React事件机制

揭秘React形成合成事件的过程

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

React 事件系统

React事件机制 - 源码概览(上)

react 事件池

React 合成事件和原生事件的区别

React的事件机制

React event实现原理

为什么需要在 React 类组件中为事件处理程序绑定 this

React合成事件系统

【译】了解React源代码-UI更新(DOM树)9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值