React事件机制-v16.13.1
概览
所谓事件机制,无非就是注册事件与分发事件两个步骤,程序员通过代码来注册事件到react对应的节点上,用户与浏览器发生交互,触发浏览器的原生事件,原生事件被react捕获,从原生事件中拿到原生节点,然后根据原生节点拿到react设计的fiber节点,然后从fiber节点中取出相应的回调来执行它,简单流程就是如此,所以我们这次讨论react事件机制,就是如何注册,以及如何分发。
取出事件
react初始化过程,简单来讲就是我们的jsx被babel转成一串json,我们将这串json叫做react元素的树,react再将这串json转成fiber树(fiber树主要用来做diff),生成fiber树的过程其实就是,我们对react元素的树进行一个深度优先的遍历,遍历的过程中,对于每个节点,我们所绑定的事件其实是作为一个属性绑定在上面的,遍历完成后,我们需要生成一颗真实DOM的树,生成的时候,每个真实DOM节点都会和fiber节点关联起来(通过一个key值internalInstanceKey)
注册事件
在我们生成真实DOM树的时候,需要将fiber树的一些属性(比如样式)映射到真实DOM上,当发现这个节点绑定了事件的时候,我们直接将这个事件类型绑定到根节点或者document上。
1 function addEventBubbleListener(target, eventType, listener) { |
react监听的是根节点的事件,所以listener是一个能够处理所有节点的响应事件的函数,最终浏览器中注册的事件应该是下面这样,每个事件类型,都在根节点上注册了一个相应的listener进行事件的分发。
分发事件
分发事件,简单来讲就是事件被触发后,从内存取出对应的回调来执行,当用户与浏览器产生交互时,以下事情会先后执行。
拿到原生event
触发原生事件,拿到原生事件的event。
完整代码地址:addEventCaptureListener[1]
1 document.addEventListener(type, dispatchEvent) |
拿到fiber节点
根据原生事件的event可以拿到原生DOM节点,继而拿到其fiber节点。(在生成真实dom树的时候,dom节点就已经通internalInstanceKey和fiber节点关联起来了)
完整代码地址:attemptToDispatchEvent[2]
1 const nativeEventTarget = getEventTarget(nativeEvent); |
合成react事件
根据原生事件开始合成react事件,在此之前,先大概介绍以下react合成事件这个概念。
核心概念
浏览器的事件,大多数直接派生自Event[3],另外有7类属于UI事件,派生自UIEvent[4],UIEvent[5]派生自Event[6]。react内部也是如此。
细节就不再过多赘述了,主要做的事是
1.对原生事件的封装,基本上是把原生事件的一些属性在代码内部自己定义了一遍。2.对某些原生事件(change,select,beforeInput等)的升级和改造,这类事件注册时会附带注册一些依赖项,例如,给input注册了onchange事件,那么”blur”, “change”, “click”, “focus”, “input”, “keydown”, “keyup”, “selectionchange”这些事件全都会被注册,原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。3.不同浏览器事件兼容的处理。
1 const nativeEventTarget = getEventTarget(nativeEvent); |
合成过程
1.根据事件类型选择插件进行合成(react将所有事件归纳进了六种插件,事件的合成由插件进行)代码地址:extractEvents[7]2.根据事件的类型,实例化不同React事件的构造函数进行合成,代码地址: SimpleEventPlugin.extractEvents [8]
1 let EventConstructor; |
累积所有实例和侦听器
1.根据fiber节点找到对应事件的回调函数
1 function getListener() { |
1.根据捕获或冒泡阶段给事件队列listeners添加回调函数
•先将捕获事件的回调放入listeners里•如果是冒泡和捕获阶段都需要触发的事件,则放到listener的头部(unshift)•如果不是,捕获阶段的事件放到listener的尾部(pop)
1 const listeners: Array<DispatchListener> = []; |
\3. 节点一直向上遍历,对于每个节点重复执行1和2
1 let instance = targetFiber; |
触发回调
现在listeners里包含了所有的侦听器,循环执行,将其分发出去。
1 if (listeners.length !== 0) { |
另外有一点是当其中有某一个回调执行了stopPropagation后,后续的代码就不会执行了。
References
[1]
addEventCaptureListener: https://github.com/mini-peanut/react/blob/master/packages/react-dom/src/events/DOMModernPluginEventSystem.js#L513[2]
attemptToDispatchEvent: https://github.com/mini-peanut/react/blob/master/packages/react-dom/src/events/ReactDOMEventListener.js#L269[3]
Event: https://developer.mozilla.org/zh-CN/docs/Web/API/Event[4]
UIEvent: https://developer.mozilla.org/zh-CN/docs/Web/API/UIEvent[5]
UIEvent: https://developer.mozilla.org/zh-CN/docs/Web/API/UIEvent[6]
Event: https://developer.mozilla.org/zh-CN/docs/Web/API/Event[7]
extractEvents: https://github.com/mini-peanut/react/blob/master/packages/react-dom/src/events/DOMModernPluginEventSystem.js#L132[8]
SimpleEventPlugin.extractEvents : https://github.com/mini-peanut/