1.背景
源码版本号为17.0.3,代码经常在变,可能你看到的跟我有所不同
react自己实现了一套事件委托机制,用合成事件替代了原生事件,模拟量冒泡捕获过程,
主要优点有 1.实现事件优先级。2.抹平浏览器差异
react17跟之前版本不同,把非委派事件绑定在root节点上,弱化了simpleEventPlugin的概念。。
2.实现
事件是怎么注册的?
1.对于需要委托的事件
jsx中用<div onClick={this.handle}></div>写事件绑定,实际上onClick方法会传入fiber的props中,并不像原生事件监听。
初始化时,在react-dom上下文中首先通过registerEvents方法把react事件名称跟对应的dependencies关系存到registrationNameDependencies中:维护一个数据结构 {'onclick':['click'],... }
并把所有的依赖存入allNativeEvents中,这里只会存一些需要委派的事件如click ,非委派的(主要是媒体事件)会绑在自己对应的el节点上,后文会讲到
function registerDirectEvent(registrationName, dependencies) {
...
registrationNameDependencies[registrationName] = dependencies;
{
var lowerCasedName = registrationName.toLowerCase();
possibleRegistrationNames[lowerCasedName] = registrationName;
if (registrationName === 'onDoubleClick') {
possibleRegistrationNames.ondblclick = registrationName;
}
}
for (var i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
在创建发fiberRoot根节点时createRootImpl调用listenToAllSupportedEvents,遍历所有allNativeEvents,根据事件类型执行方法listenToNativeEvent,这个方法实际上就调用了addTrappedEventListener方法,获取对应优先级的dispatchevent,用bind函数wrapper一下对这个listener绑定他的eventSystemFlags.最后通过我们熟悉的addEventlistener到root节点中去
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
var isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
if (domEventName === 'touchstart' || domEventName === 'touchmove' || domEventName === 'wheel') {
isPassiveListener = true;
}
}
targetContainer = targetContainer;
var unsubscribeListener; // When legacyFBSupport is enabled, it's for when we
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
}
}
}
react-dom中对于每个原生事件都维护了对应的优先级。对于每个事件优先级的对应不同的dispatchEvent:『DiscreteEventPriority:dispatchDiscreteEvent,ContinuousEventPriority:dispatchContinuousEvent,DefaultEventPriority:dispatchEvent』,对于click使用的是DiscreteEventPriority,对应的lane为synclane=1
2.对于非委派事件主要有这些类型
var mediaEventTypes = ['abort', 'canplay', 'canplaythrough', 'durationchange', 'emptied', 'encrypted', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting'];
var nonDelegatedEvents = new Set(['cancel', 'close', 'invalid', 'load', 'scroll', 'toggle'].concat(mediaEventTypes));
在completeWork阶段,对于hostComponent会执行finalizeInitialChildren方法
首先根据el的tag,即hostComponent fiber的type 执行listenToNonDelegatedEvent方法实现监听
function listenToNonDelegatedEvent(domEventName, targetElement) {
{
if (!nonDelegatedEvents.has(domEventName)) {
error('Did not expect a listenToNonDelegatedEvent() call for "%s". ' + 'This is a bug in React. Please file an issue.', domEventName);
}
}
var isCapturePhaseListener = false;
var listenerSet = getEventListenerSet(targetElement);
var listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
if (!listenerSet.has(listenerSetKey)) {
addTrappedEventListener(targetElement, domEventName, IS_NON_DELEGATED, isCapturePhaseListener);
listenerSet.add(listenerSetKey);
}
}
逻辑就是对比维护的delegatedEvents列表 把事件名存入该el的listenerSet,后续逻辑跟上面差不多,根据事件优先级在冒泡或捕获阶段绑定对应的dispatchEvent事件
事件是怎么触发的?
以onClick举例,会调用之前的dispatchDiscreteEvent.bind() 以后的方法,里面的逻辑就是执行一遍未执行完的discreteEvent事件,然后调用dispatchEvent,dispatchevent里面也会刷一遍优先级更高的事件,然后走到如下方法。
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
...
batchedEventUpdates(function () {
return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
}
batchedEventUpdates方法主要是打开批处理开关的,对于已经打开isBatchingEventUpdates开关的执行dispatchEventsForPlugins函数 对于未开的打开开关,先打开开关后执行dispatchEventsForPlugins再重置react的render时间跟用调度器刷一下队列,最后关上开关。用这种方式可以对短时间多次触发批处理。 dispatchEventsForPlugins作用是创建合成事件推入dispatchQueue队列,然后遍历执行
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
var nativeEventTarget = getEventTarget(nativeEvent);
var dispatchQueue = [];
extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents主要逻辑是根据事件名找到对应的合成方法生成合成事件event,获取他是冒泡或捕获,执行accumulateSinglePhaseListeners方法,根据他是捕获事件还是冒泡事件获取不同的事件名称,遍历instance找对应props中名称一致的方法,积累到listeners上去
这会导致onClick onClickCapture 在两条listener上
listeners的数据结构:[{instance: FiberNode, currentTarget: div, listener: ƒ}]
大致代码:
function extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
var reactName = topLevelEventsToReactNames.get(domEventName);
var SyntheticEventCtor = SyntheticEvent;
var reactEventType = domEventName;
switch (domEventName) {
case 'keypress':
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent;
case 'click':
SyntheticEventCtor = SyntheticMouseEvent;
break;
...
}
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
{
var accumulateTargetOnly = !inCapturePhase && // TODO: ideally, we'd eventually add all events from
domEventName === 'scroll';
var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
if (_listeners.length > 0) {
// Intentionally create event lazily.
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: _event,
listeners: _listeners
});
}
}
}
然后执行processDispatchQueue,方法根据inCapturePhase模拟冒泡或者捕获,从左或右遍历listeners 遇到instance有event.isPropagationStopped()的停止执行,最后调用executeDispatch执行函数