react事件机制
原生js事件(浏览器的事件系统)
在学习React事件机制之前,我们先回顾一下js的原生事件系统,这对我们理解react事件系统非常重要。
1.事件的分类:
在《JavaScript权威指南》这本书中,把事件分为了四类:
1).表单事件,submit、reset、click、change、focus和blur等事件,其中focus和blur事件不会冒泡,在IE浏览器中定义了focusin和focusout来取代focus和blur,并且可以冒泡。input事件
2).window事件,
3).鼠标事件,mouseover,mouseout,mousedown、mouseup mousemove,click
4).键盘事件,keydown,keyup,keypress
2.事件流
事件流描述的是从页面中接受事件的顺序,如果你单击了某个按钮,那么单击事件不仅仅发生在按钮上,换句话说,在单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。
1)IE事件流(事件冒泡):
即事件开始由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点
2)Netscape事件流(事件捕获):
在事件捕获过程中,document 对象首先接收到 click 事件,然后事件沿DOM树依次向下,一直 传播到事件的实际目标,即<div>
元素。
3)DOM事件流
当我们点击了一个事件, 首先做的的第一件事就是从外层的元素,直接穿梭到我们的目标元素。这个阶段会执行所有捕获阶段的函数,然后事件流切换到目标阶段,执行自身的事件函数,这时候事件流在沿着相反的方向一直向上执行所有函数。如果想阻止事件的传播,可以在指定节点的事件监听器通过event.stopPropagation()
或event.cancelBubble = true
阻止事件传播。有些事件是没有冒泡阶段的,如scroll、blur、及各种媒体事件等。
3.绑定事件的方法
1)行内HTML事件绑定
<div onclick="handleClick()">
test
</div>
<script>
let handleClick = function(){
// 一些处理代码..
}
// 移除事件
handleClick = function(){}
</script>
缺点:js和HTML代码耦合了;时差问题,因为用户可能会在 HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。
2)事件处理器属性(DOM0)
这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。
<div id="test">
test
</div>
<script>
let target = document.getElementById('test')
// 绑定事件
target.onclick = function(){
alert(this);
// 一些处理代码..
}
target.onclick = function(){
// 另外一些处理代码...会覆盖上面的
}
// 移除事件
target.onclick = null
</script>
缺点:作为属性使用,一次只能绑定一个事件,多次赋值会覆盖,只能处理冒泡阶段
3)addEventListener(DOM2)
<div id="test">
test
</div>
<script>
let target = document.getElementById('test')
// 绑定事件
let funcA = function(){
// 一些处理代码..
}
let funcB = function(){
// 一些处理代码..
}
// 添加冒泡阶段监听器
target.addEventListener('click',funcA,false)
// 添加捕获阶段监听器
target.addEventListener('click',funcB,true)
// 移除监听器
target.removeEventListener('click', funcA)
</script>
优点:是可以添加多个事件处理程序,
缺点:无法移除匿名绑定的匿名函数
4)IE的事件处理程序——》跨浏览器的事件处理程序
由于事件处理程序可以为现代 Web 应用程序提供交互能力,因此许多开发人员会不分青红皂白地向页面中添加大量的处理程序。在JavaScript中,添加到页面上 的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的 角度出发,还是有一些方法能够提升性能的
4.事件委托
对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会一直冒泡到document层次。也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事 件处理程序。
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
var list = document.getElementById("myLinks");
list.addEventListener("click", function(event){
switch(event.id){
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http://www.wrox.com";
break;
case "sayHi":
alert("hi");
break;
}
});
为什么React实现了自己的事件机制
- 将事件都代理到了根节点上,减少了事件监听器的创建,节省了内存
- 磨平浏览器差异,开发者无需兼容多种浏览器写法。如想阻止事件传播时需要编写
event.stopPropagation()
或event.cancelBubble = true
,在React中只需编写event.stopPropagation()
即可。 - 对开发者友好。只需在对应的节点上编写如
onClick
、onClickCapture
等代码即可完成click
事件在该节点上冒泡节点、捕获阶段的监听,统一了写法。
1.事件分类:
React对在React中使用的事件进行了分类,具体通过各个类型的事件处理插件分别处理:
react-dom-bindings/src/events/plugins
SimpleEventPlugin
简单事件,代表事件onClick
BeforeInputEventPlugin
输入前事件,代表事件onBeforeInput
ChangeEventPlugin
表单修改事件,代表事件onChange
EnterLeaveEnventPlugin
鼠标进出事件,代表事件onMouseEnter
SelectEventPlugin
选择事件,代表事件onSelect
这里的分类是对React事件进行分类的,简单事件如onClick
和onClickCapture
,它们只依赖了原生事件click
。而有些事件是由React统一包装给用户使用的,如onChange
,它依赖了['change','click','focusin','focusout','input','keydown','keyup','selectionchange']
,这是React为了兼容不同表单的修改事件收集,如对于<input type="checkbox" />
和<input type="radio" />
开发者原生需要使用click
事件收集表单变更后的值,而在React中可以统一使用onChange
来收集。
分类并不代表依赖的原生事件之间没有交集。 如简单事件中有onKeyDown
,它依赖于原生事件keydown
。输入前事件有onCompositionStart
,它也依赖了原生事件keydown
。表单修改事件onChange
,它也依赖了原生事件keydown
2.事件收集
原生js事件
const simpleEventPluginEvents = [
'abort',
'auxClick',
'cancel',
'canPlay',
'canPlayThrough',
'click',
'close',
'contextMenu',
'copy',
'cut',
'drag',
'dragEnd',
'dragEnter',
'dragExit',
'dragLeave',
'dragOver',
'dragStart',
'drop',
'durationChange',
'emptied',
'encrypted',
'ended',
'error',
'gotPointerCapture',
'input',
'invalid',
'keyDown',
'keyPress',
'keyUp',
'load',
'loadedData',
'loadedMetadata',
'loadStart',
'lostPointerCapture',
'mouseDown',
'mouseMove',
'mouseOut',
'mouseOver',
'mouseUp',
'paste',
'pause',
'play',
'playing',
'pointerCancel',
'pointerDown',
'pointerMove',
'pointerOut',
'pointerOver',
'pointerUp',
'progress',
'rateChange',
'reset',
'resize',
'seeked',
'seeking',
'stalled',
'submit',
'suspend',
'timeUpdate',
'touchCancel',
'touchEnd',
'touchStart',
'volumeChange',
'scroll',
'toggle',
'touchMove',
'waiting',
'wheel',
];
react-dom-bindings/src/events/DOMEventProperties.js和react-dom-bindings/src/events/EventRegistry.js
export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = ((simpleEventPluginEvents[i]: any): string);
const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
}
}
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
) {
registrationNameDependencies[registrationName] = dependencies;
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
由于React需要对所有的事件做代理委托,所以需要事先知道浏览器支持的所有事件,这些事件都是硬编码在React源码的各个事件插件中的。
而对于所有需要代理的原生事件,都会以原生事件名 字符串的形式存储在一个名为allNativeEvents
的集合中,并且在registrationNameDependencies
中存储React事件名到其依赖的原生事件名数组的映射。
import {
topLevelEventsToReactNames,
registerSimpleEvents,
} from '../DOMEventProperties';
export {registerSimpleEvents as registerEvents, extractEvents};
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
而事件的收集是通过各个事件处理插件各自收集注册的,在页面加载时,会执行各个插件的registerEvents
,将所有依赖的原生事件都注册到allNativeEvents
中去,并且在registrationNameDependencies
中存储映射关系。
registrationNameDependencies = {
onBlur: ['blur'],
onClick: ['click'],
onClickCapture: ['click'],
onChange: ['blur', 'change', 'click', 'focus','input','keydown','keyup',
'selectionchange'],
onMouseEnter: ['mouseout', 'mouseover'],
onMouseLeave: ['mouseout', 'mouseover'],
...
}
allNativeEvents = ['cancel','click', ...]
对于原生事件不支持冒泡阶段的事件,硬编码的形式存储在了nonDelegatedEvents
集合中,原生不支持冒泡阶段的事件在后续的事件代理环节有不一样的处理方式。
后面的描述中,对于nonDelegatedEvents,称为非代理事件。其他的事件称为代理事件。他们的区别在于原生事件是否支持冒泡
nonDelegatedEvents: Set<DOMEventName> = new Set([
'cancel',
'close',
'invalid',
'load',
'scroll',
'toggle',
// In order to reduce bytes, we insert the above array of media events
// into this Set. Note: the "error" event isn't an exclusive media event,
// and can occur on other elements too. Rather than duplicate that event,
// we just take it from the media events array.
...mediaEventTypes,
]);//有29种事件
3.事件代理
将事件委托代理到根的操作发生在ReactDOM.render(element, container)
时。
1).可代理事件
在ReactDOM.render
的实现中,在创建了fiberRoot
后,在开始构造fiber
树前,会调用listenToAllSupportedEvents
进行事件的绑定委托。
listenToNativeEvent
即对元素进行事件绑定的方法,第二个参数的含义是是否将监听器绑定在捕获阶段。 由此我们可以看到,对于不存在冒泡阶段的事件,React只委托了捕获阶段的监听器,而对于其他的事件,则对于捕获阶段和冒泡阶段都委托了监听器。在listenToNativeEvent
里面又调用了addTrappedEventListener
,在addTrappedEventListener
内部调用createEventListenerWrapperWithPriority
获得绑定了入参的dispatchEvent,使用addEventListtener绑定到根元素上。
const listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
// 避免重复挂载在根节点上
if (!(rootContainerElement: any)[listeningMarker]) {
// 将该根元素标记为已初始化事件监听
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}
此时返回的listenerWrapper.bind
绑定了三个参数,此时的触发对象为null,而返回的这个函数就是dispatch绑定在了root节点上,当用户触发这个函数时,此时null就指向了当前的节点,所以真正的事件源对象event
,被默认绑定成第四个参数。
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
2)非代理事件
对于非代理事件nonDelegatedEvents
,由于这些事件不存在冒泡阶段,所以我们在根部代理他们的冒泡阶段监听器也不会触发,所以需要特殊处理。
实际上这些事件的代理发生在DOM实例的创建阶段,也就是render
阶段的completeWork
阶段。通过调用finalizeInitialChildren
为DOM实例设置属性时,判断DOM节点类型来添加响应的冒泡阶段监听器。 如为<img />
和<link />
标签对应的DOM实例添加error
和load
的监听器。
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
):void {
// ...
switch (tag) {
// ...
case 'img':
case 'image':
case 'link':
listenToNonDelegatedEvent('error', domElement);
listenToNonDelegatedEvent('load', domElement);
break;
// ...
}
// ...
}
// 非代理事件监听器绑定
export function listenToNonDelegatedEvent(
domEventName: DOMEventName,
targetElement: Element,
): void {
// 绑定在目标/冒泡阶段
const isCapturePhaseListener = false;
const listenerSet = getEventListenerSet(targetElement);
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
if (!listenerSet.has(listenerSetKey)) {
addTrappedEventListener(
targetElement,
domEventName,
IS_NON_DELEGATED,// 非代理事件
isCapturePhaseListener,// 目标/冒泡阶段
);
listenerSet.add(listenerSetKey);
}
}
图示:img
元素上绑定了非代理事件error
和load
的冒泡阶段回调,实际上React对这些不可冒泡的事件都进行了冒泡模拟。
4.事件合成
合成事件SyntheticEvent
是React事件系统对于原生事件跨浏览器包装器。它除了兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()
和 preventDefault()
。比如onClick事件回调方法中的参数e,是经过react包装过的,同时原生事件对象被放在了属性e.nativeEvent内,如果你发现由于某些原因需要使用一些底层的浏览器事件,只需用nativeEvent的属性来获得。
nativeEvent是原生底层浏览器事件,其余的是SyntheicEvent对象的属性。
1)磨平浏览器差异
React通过事件normalize
以让他们在不同浏览器中拥有一致的属性。
React声明了各种事件的接口,以此来磨平浏览器中的差异:
- 如果接口中的字段值为0,则直接使用原生事件的值
- 如果接口中字段的值为函数,则会以原生事件作为入参,调用该函数来返回磨平了浏览器差异的值。
// 基础事件接口
const EventInterface = {
eventPhase: 0,
bubbles: 0,
cancelable: 0,
timeStamp: function (event: {[propName: string]: mixed}) {
return event.timeStamp || Date.now();
},
defaultPrevented: 0,
isTrusted: 0,
};
// 鼠标事件,继承UI事件接口,其中relatedTarget、movementX、movementY需要磨平差异
const MouseEventInterface: EventInterfaceType = {
...UIEventInterface,
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
pageX: 0,
pageY: 0,
ctrlKey: 0,
shiftKey: 0,
altKey: 0,
metaKey: 0,
getModifierState: getEventModifierState,
button: 0,
buttons: 0,
relatedTarget: function (event) {
if (event.relatedTarget === undefined)
return event.fromElement === event.srcElement
? event.toElement
: event.fromElement;
return event.relatedTarget;
},
movementX: function (event) {
if ('movementX' in event) {
return event.movementX;
}
updateMouseMovementPolyfillState(event);
return lastMovementX;
},
movementY: function (event) {
if ('movementY' in event) {
return event.movementY;
}
// Don't need to call updateMouseMovementPolyfillState() here
// because it's guaranteed to have already run when movementX
// was copied.
return lastMovementY;
},
};
export const SyntheticMouseEvent: $FlowFixMe = createSyntheticEvent(MouseEventInterface);
由于不同的类型的事件其字段有所不同,所以React实现了针对事件接口的合成事件构造函数的工厂函数。 通过传入不一样的事件接口返回对应事件的合成事件构造函数,然后在事件触发回调时根据触发的事件类型判断使用哪种类型的合成事件构造函数来实例化合成事件。
// 合成事件构造函数的工厂函数,根据传入的事件接口返回对应的合成事件构造函数
function createSyntheticEvent(Interface: EventInterfaceType) {
function SyntheticBaseEvent(
reactName: string | null,
reactEventType: string,
targetInst: Fiber | null,
nativeEvent: {[propName: string]: mixed, ...},
nativeEventTarget: null | EventTarget,
) {
// react事件名
this._reactName = reactName;
// 当前执行事件回调时的fiber
this._targetInst = targetInst;
// 真实事件名
this.type = reactEventType;
// 原生事件对象
this.nativeEvent = nativeEvent;
// 原生触发事件的DOM target
this.target = nativeEventTarget;
// 当前执行回调的DOM
this.currentTarget = null;
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
this[propName] = nativeEvent[propName];
}
}
const defaultPrevented =
nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
// $FlowFixMe[prop-missing] found when upgrading Flow
assign(SyntheticBaseEvent.prototype, {
// $FlowFixMe[missing-this-annot]
preventDefault: function () {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.preventDefault) {
event.preventDefault();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
// $FlowFixMe[missing-this-annot]
stopPropagation: function () {
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
persist: function () {
// Modern event system doesn't use pooling.
},
isPersistent: functionThatReturnsTrue,
});
return SyntheticBaseEvent;
}
5.事件执行(dispatchEvent)
当页面上触发了特定的事件时,如点击事件click,就会触发绑定在根元素上的事件回调函数,也就是之前绑定了参数的dispatchEvent
,而dispatchEvent
在内部最终会调用dispatchEventsForPlugins
,看一下dispatchEventsForPlugins
具体做了哪些事情。
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
里面执行了extractEvents
和processDispatchQueue
,extractEvents
是用来收集对应的回调事件
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
EnterLeaveEventPlugin.extractEvents,
ChangeEventPlugin.extractEvents,
SelectEventPlugin.extractEvents,
BeforeInputEventPlugin.extractEvents
...
}
以SimpleEventPlugin.extractEvents
为例看看如何进行收集,通过domEventName找到对应的事件合成器,然后通过accumulateSinglePhaseListeners
来计算从触发当前事件的节点到根节点的所有事件回调,然后调用合成器,将合成事件和收集的所有回调push到dispatchQueQueue
中,等到回调的执行。
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
switch (domEventName) {
case 'keypress':
if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
return;
}
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case 'focusin':
reactEventType = 'focus';
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'focusout':
reactEventType = 'blur';
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'beforeblur':
case 'afterblur':
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'click':
if (nativeEvent.button === 2) {
return;
}
case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup':
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent;
break;
case 'drag':
case 'dragend':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'dragstart':
case 'drop':
SyntheticEventCtor = SyntheticDragEvent;
break;
case 'touchcancel':
case 'touchend':
case 'touchmove':
case 'touchstart':
SyntheticEventCtor = SyntheticTouchEvent;
break;
case ANIMATION_END:
case ANIMATION_ITERATION:
case ANIMATION_START:
SyntheticEventCtor = SyntheticAnimationEvent;
break;
case TRANSITION_END:
SyntheticEventCtor = SyntheticTransitionEvent;
break;
case 'scroll':
SyntheticEventCtor = SyntheticUIEvent;
break;
case 'wheel':
SyntheticEventCtor = SyntheticWheelEvent;
break;
case 'copy':
case 'cut':
case 'paste':
SyntheticEventCtor = SyntheticClipboardEvent;
break;
case 'gotpointercapture':
case 'lostpointercapture':
case 'pointercancel':
case 'pointerdown':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerup':
SyntheticEventCtor = SyntheticPointerEvent;
break;
default:
break;
}
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
if (
enableCreateEventHandleAPI &&
eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
) {
const listeners = accumulateEventHandleNonManagedNodeListeners(
((reactEventType: any): DOMEventName),
targetContainer,
inCapturePhase,
);
if (listeners.length > 0) {
// Intentionally create event lazily.
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
} else {
const accumulateTargetOnly =
!inCapturePhase &&
domEventName === 'scroll';
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
if (listeners.length > 0) {
// Intentionally create event lazily.
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueQueue.push({event, listeners});
}
}
}
在accumulateSinglePhaseListeners
中,通过循环目标节点的fiber,收集从target到root的所有回调。
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
nativeEvent: AnyNativeEvent,
): Array<DispatchListener> {
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
let listeners: Array<DispatchListener> = [];
let instance = targetFiber;
let lastHostComponent = null;
// Accumulate all instances and listeners via the target -> root path.
while (instance !== null) {
const {stateNode, tag} = instance;
// Handle listeners that are on HostComponents (i.e. <div>)
if (
(tag === HostComponent ||
(enableFloat ? tag === HostHoistable : false) ||
(enableHostSingletons ? tag === HostSingleton : false)) &&
stateNode !== null
) {
lastHostComponent = stateNode;
// createEventHandle listeners
if (enableCreateEventHandleAPI) {
const eventHandlerListeners =
getEventHandlerListeners(lastHostComponent);
if (eventHandlerListeners !== null) {
eventHandlerListeners.forEach(entry => {
if (
entry.type === nativeEventType &&
entry.capture === inCapturePhase
) {
listeners.push(
createDispatchListener(
instance,
entry.callback,
(lastHostComponent: any),
),
);
}
});
}
}
// Standard React on* listeners, i.e. onClick or onClickCapture
if (reactEventName !== null) {
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
}
// 返回当前节点的父级节点,直到节点不存在跳出循环
instance = instance.return;
}
return listeners;
}
执行dispatchQueQueue
中的所有回调,可以看到对于回调的处理,就是简单地根据收集到的回调数组,判断事件的触发是处于捕获阶段还是冒泡阶段来决定是顺序执行还是倒序执行回调数组。并且通过event.isPropagationStopped()
来判断事件是否执行过event.stopPropagation()
以决定是否继续执行。
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags,
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0; i < dispatchQueue.length; i++) {
const {event, listeners} = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
// event system doesn't use pooling.
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
总结下来就是:
- 执行dispatchEvent
- 创建事件对应的合成事件 SyntheticEvent
- 收集捕获的回调函数和对应的节点实例
- 收集冒泡的回调函数和对应的节点实例
- 执行对应的回调函数,同时将SyntheticEvent 作为参数传入
react16及更早版本和react17、react18的区别
1.当事件池遇到异步时,会有问题。
handerClick = (e) => {
console.log(e.target) // button
setTimeout(()=>{
console.log(e.target) // null
},0)
}
因为在React16以及之前版本中采取了一个事件池的概念,每次我们用的事件源对象,在事件函数执行之后,可以通过releaseTopLevelCallbackBookKeeping
等方法将事件源对象释放到事件池中,这样的好处每次我们不必再创建事件源对象,可以从事件池中取出一个事件源对象进行复用,在事件处理函数执行完毕后,会释放事件源到事件池中,清空属性,这就是setTimeout
中打印为什么是null
的原因了。所以react17以及之后的版本已经摒弃了事件池。
2.事件绑定在了root上,而非document上,
事件统一绑定container上,ReactDOM.render(app, container);而不是document上,这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在document
上,那么可能多应用下会出现问题。