Appearance
事件的绑定
对于我们组件中对于某一个元素绑定一个事件的时候,这是怎么注册为React的合成事件的?
FiberNode事件的绑定是发生在completeWork阶段,在节点构建DOM元素的过程中 其经历了一下几个过程
createInstance()
appendAllChildren(instance, workInProgress, false, false);
finalizeInitialChildren()
finalizeInitialChildren()
的过程主要就是处理DOM元素的属性的过程,在处理属性的过程中如遇到事件类型的属性,就会触发事件的绑定流程。
js
/**
* 减Props的内容更新到 DOM元素 上
* 此处主要处理以下:
* - style 属性
* - dangerouslySetInnerHTML
* - children属性
* - autoFocus
* - 其他 初始化注册的 事件 onClick -- 通过 ensureListeningTo 绑定事件
* @param {*} tag
* @param {*} domElement
* @param {*} rootContainerElement
* @param {*} nextProps
* @param {*} isCustomComponentTag
*/
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
// 事件的回调得是函数类型的
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
// enableEagerRootListeners 默认为true,解决createPotal创建的container的问题
if (!enableEagerRootListeners) {
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
}
}
}
从源码中可以得出,在处理FiberNode的DOM元素的属性时,其没有处理事件类型的属性(onClick, 注意这里处理 onScroll事件),那么onClick是怎么注册到 button元素上,怎么触发的呐?
前置知识
这就涉及到浏览签的事件体系
事件可以分为捕获与冒泡两个阶段,其事件的起点和终点都是window对象,目标对象都是事件触发的元素本身(target) 。那么是否就可以通过事件委托机制,在一个根元素上注册了所有的事件,然后通过 e.target
去判断触发事件的目标对象。
React就是模拟的浏览器事件流的机制,不在元素节点上去注册事件,而是通过将所有的事件全部都注册到React16的document对象|React17的root对象上,然后通过 e.target寻找目标DOM,然后转换成目标的FiberNode对象,然以通过不同的 任务优先级去执行事件回调方法。这就是React的事件池的概念
通过上述completeWork阶段和React事件池概念的讲解,我们再回到事件系统的初始化流程去,这时候我们就会发现,在React初始化的时候,其不是初始化事件机制,其实也是绑定了所有的事件。
事件初始化及绑定流程
其核心方法是
js
// 根据事件的任务级别去构建不同的事件监听函数
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
下面我们细细看这个方法
createEventListenerWrapperWithPriority()
React的一大特点就是任务调度,对于事件体系其触发回调也是通过任务调度的方式进行的。
在事件体系中,其将所有的任务分为3个任务等级()
js
export const DiscreteEvent: EventPriority = 0;
export const UserBlockingEvent: EventPriority = 1;
export const ContinuousEvent: EventPriority = 2;
DiscreteEvent
: 最高优先级的事件任务,比如我们大部分的事件 click , input , keyDowndispatchDiscreteEvent()进行任务调度
UserBlockingEvent
: 优先级适中, 包括drag, scroll
等事件dispatchUserBlockingUpdate()进行任务调度
ContinuousEvent
: 优先级最低 ,包括animation, load
等事件dispatchEvent()进行任务调度
源码在:
js
/**
* 根据任务优先级进行事件的注册
*/
export function registerSimpleEvents() {
// 最高优先级事件任务 - 0
registerSimplePluginEventsAndSetTheirPriorities(
discreteEventPairsForSimpleEventPlugin, // 注册的最高优先级任务列表
DiscreteEvent, // 任务的优先级 - 0
);
// 次优先级事件任务 - 1
registerSimplePluginEventsAndSetTheirPriorities(
userBlockingPairsForSimpleEventPlugin,
UserBlockingEvent,
);
// 最低优先级事件任务 - 2
registerSimplePluginEventsAndSetTheirPriorities(
continuousPairsForSimpleEventPlugin,
ContinuousEvent,
);
// 将一些不能通过事件代理的方式处理的事件,单独处理
// selectionchange
setEventPriorities(otherDiscreteEvents, DiscreteEvent);
}
下面我们具体看一下三种不同优先级的任务具体哪里不同
js
/**
* 最高优先级的事件任务 调度的事件处理
* @param {*} domEventName
* @param {*} eventSystemFlags
* @param {*} container
* @param {*} nativeEvent
*/
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
// discreteUpdates的方式触发dispatchEvent
discreteUpdates(
dispatchEvent,
domEventName,
eventSystemFlags,
container,
nativeEvent,
);
}
function dispatchUserBlockingUpdate(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
// runWithPriority 触发dispatchEvent的回调
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
container,
nativeEvent,
),
);
}
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
}
可见其任务的回调都是通过 dispatchEvent 进行,
- 最高优先的通过
dispatchDiscreteEvent()
创建了一个isInsideEventHandler
执行环境 - 次优先级的通过 runWithPriority 去调用Scheduler任务调度,优先级为 1
- 最低的 直接触发 dispatchEvent
dispatchDiscreteEvent()
栗子
js
const Child = () => {
const [visible, setVisible] = React.useState(true)
const handleClickBtn = () => {
setVisible(!visible)
}
return (
<div>
<button onClick={handleClickBtn}>切换</button>
</div>
)
}
const App = props => {
const handleClickBtn = () => {
console.log("App")
}
return (
<div onClick={handleClickBtn}>
<Child />
</div>
)
}
其结果如下