【React进阶系列】史上最全React事件机制详解

【React进阶系列】史上最全React事件机制详解_第1张图片
React事件机制全览.png

框架总览


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

DOM事件流的三个阶段

【React进阶系列】史上最全React事件机制详解_第2张图片
事件流.jpg

1、事件捕获阶段
当某个事件触发时,文档根节点最先接受到事件,然后根据DOM树结构向具体绑定事件的元素传递。该阶段为父元素截获事件提供了机会。
事件传递路径为:
window —> document —> boy —> div—> text

2、目标阶段
具体元素已经捕获事件。之后事件开始向根节点冒泡。

3、事件冒泡阶段
该阶段的开始即是事件的开始,根据DOM树结构由具体触发事件的元素向根节点传递。
事件传递路径:
text—> div —> body —> document —> window

使用addEventListener函数在事件流的的不同阶段监听事件。
DOMEle.addEventListener(‘事件名称’,handleFn,Boolean);
此处第三个参数Boolean即代表监听事件的阶段;
为true时,在在捕获阶段监听事件,执行逻辑处理;
为false时,在冒泡阶段监听事件,执行逻辑处理。

【React进阶系列】史上最全React事件机制详解_第3张图片
关于React事件的疑问1.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 B
2. 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注册事件的时候也是对兼容性做了处理。


【React进阶系列】史上最全React事件机制详解_第4张图片
image.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 合成事件与原生事件执行顺序图:


【React进阶系列】史上最全React事件机制详解_第5张图片
3853478932-5a9ff2f3efa39_articlex.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的事件机制源码看整个流程

【React进阶系列】史上最全React事件机制详解_第6张图片
从React的事件机制源码看整个流程

基本流程

在 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   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |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。

【React进阶系列】史上最全React事件机制详解_第7张图片
image.png

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

事件注册

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

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

大致流程

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

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

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


【React进阶系列】史上最全React事件机制详解_第8张图片
image.png
关键步骤

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

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

a. 完成事件注册

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

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

【React进阶系列】史上最全React事件机制详解_第9张图片
image.png

源码解析

【React进阶系列】史上最全React事件机制详解_第10张图片
事件注册.jpg

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)

【React进阶系列】史上最全React事件机制详解_第11张图片
image.png

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

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

_updateDOMProperties: function (lastProps, nextProps, transaction) {
    ... // 前面代码太长,省略一部分
   for (propKey in nextProps) {
      var nextProp = nextProps[propKey];
      if(propKey === STYLE){
        ...
      }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);
        }
      }else{
        ...
      } 
    }
}

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


【React进阶系列】史上最全React事件机制详解_第12张图片
image.png
事件注册和事件的存储
1.事件注册
【React进阶系列】史上最全React事件机制详解_第13张图片
image.png

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

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统一为dispatchEventdispatchEvent将在之后讲。

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

事件存储

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

【React进阶系列】史上最全React事件机制详解_第14张图片
image.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。

【React进阶系列】史上最全React事件机制详解_第15张图片
image.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进行关联,在触发阶段就可以找到相关的事件回调。

【React进阶系列】史上最全React事件机制详解_第16张图片
image.png

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

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

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


【React进阶系列】史上最全React事件机制详解_第17张图片
image.png

事件触发

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

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

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

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

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

【React进阶系列】史上最全React事件机制详解_第18张图片
image.png
【React进阶系列】史上最全React事件机制详解_第19张图片
160e542c427979ed.jpg

举个栗子

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

handleFatherClick=(e)=>{
        console.log('father click');
    }

    handleChildClick=(e)=>{
        console.log('child click');
    }

    render(){
        return 
father
child
}

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

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

image.png
1. 事件分发
【React进阶系列】史上最全React事件机制详解_第20张图片
image.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的作用看ta的定义就知道了,就是一个用来保存过程中会使用到的变量的对象。使用了 
     //react在源码中用到的对象池的方法来避免多余的垃圾回收
    function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
      this.topLevelType = topLevelType;
      this.nativeEvent = nativeEvent;
      this.ancestors = [];
    }
    Object.assign(TopLevelCallbackBookKeeping.prototype, {
      destructor: function() {
        this.topLevelType = null;
       this.nativeEvent = null;
        this.ancestors.length = 0;
      },
    });
    PooledClass.addPoolingTo(
       TopLevelCallbackBookKeeping,
       PooledClass.twoArgumentPooler
    );
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
      // 放入批处理队列中,React事件流也是一个消息队列的方式
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
}

关于getPooled,可以参考我的另一篇文章react 对象池解读
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曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,
    // 我们知道一般情况下,我们的组件最后会被包裹在
的标签里 // 一般是没有组件再去嵌套它的,所以通常返回null ancestor = ancestor && findParent(ancestor); } while (ancestor); // 从当前组件向父组件遍历,依次执行注册的回调方法. 我们遍历构造ancestors数组时,是从当前组件向父组件回溯的,故此处事件回调也是这个顺序 // 这个顺序就是冒泡的顺序 for (var i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); } } function findParent(inst) { while (inst._hostParent) { inst = inst._hostParent; } var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst); var container = rootNode.parentNode; return ReactDOMComponentTree.getClosestInstanceFromNode(container); }

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

看下ReactDOMComponent实例的内容


【React进阶系列】史上最全React事件机制详解_第21张图片
image.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. 事件合成

【React进阶系列】史上最全React事件机制详解_第22张图片
image.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.js
function 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 长啥样


【React进阶系列】史上最全React事件机制详解_第23张图片
image.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]可以获取该组件的回调函数。

【React进阶系列】史上最全React事件机制详解_第24张图片
image.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);
        };
    }
}
复制代码
【React进阶系列】史上最全React事件机制详解_第25张图片
image.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

你可能感兴趣的:(【React进阶系列】史上最全React事件机制详解)