代码都来自 React 18 源码, 大家可以放心食用
读完收获
-
学会 Dom 事件流
-
理解 事件委托
-
掌握 React 合成事件原理
Dom 事件流
事件流包含三个阶段:
- 事件捕获阶段
- 目标阶段
- 事件冒泡阶段
首先发生的是事件捕获,然后是实际的目标接收到事件,最后阶段是事件冒泡阶段。
<html>
<body>
<div>
<button></button>
</div>
</body>
</html>
事件捕获阶段
事件捕获是先由最上层节点 document
先接收事件, 然后向下传播到具体的节点 document->body->div->button
目标阶段
在目标节点上触发,称为目标阶段
//w3c浏览器:event.target
//IE: event.srcElement
let target = event.target || event.srcElement;
事件冒泡阶段
从目标节点开始 (这里是 button),然后逐级向上传播 button->div->body->document
addEventListener
// useCapture 默认是 fales,当参数是true,则在捕获阶段绑定函数,反之,在冒泡阶段绑定函数,
element.addEventListener(event, function, useCapture)
阻止冒泡
// IE
window.event.cancelBubble = true;
// w3c
event.stopPropagation();
事件代理
事件代理又称之为事件委托, 事件代理是把原本需要绑定在子元素
的事件委托给父元素
,让父元素负责事件监听和处理。
事件代理的好处是有两点:
第一点:可以大量节省内存占用,减少事件注册事件。
第二点:当新增子对象时无需再次对其绑定
为什么父元素能做到事件代理呢? 笔者认为有两点:
第一点 :事件冒泡到父元素,父元素可以订阅到冒泡事件。
第二点:可以通过 event.target
得到目标节点。不然, 父元素怎么针对不同的子节点,进行定制化事件代理。
React 合成事件原理
下面这段代码在 React 18合成事件的打印结果是:
/**
document原生捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
*/
const App = () => {
const divRef = useRef();
const pRef = useRef();
const parentBubble = () => {
console.log("父元素React事件冒泡");
};
const childBubble = () => {
console.log("子元素React事件冒泡");
};
const parentCapture = () => {
console.log("父元素React事件捕获");
};
const childCapture = () => {
console.log("子元素React事件捕获");
};
useEffect(() => {
divRef.current.addEventListener(
"click",
() => {
console.log("父元素原生捕获");
},
true
);
divRef.current.addEventListener("click", () => {
console.log("父元素原生冒泡");
});
pRef.current.addEventListener(
"click",
() => {
console.log("子元素原生捕获");
},
true
);
pRef.current.addEventListener("click", () => {
console.log("子元素原生冒泡");
});
document.addEventListener(
"click",
() => {
console.log("document原生捕获");
},
true
);
document.addEventListener("click", () => {
console.log("document原生冒泡");
});
}, []);
return (
<div ref={divRef} onClick={parentBubble} onClickCapture={parentCapture}>
<p ref={pRef} onClick={childBubble} onClickCapture={childCapture}>
事件执行顺序
</p>
</div>
);
};
Mout 阶段: 点击之前
Mout 阶段总结:点击之前 就是在 root 容器上监听了 JavaScript 所有原生事件的冒泡和捕获。
第一:通过调用 SimpleEventPlugin.registerEvents
插件函数来注册事件(在项目当中的 index.tsx 中 调用 createRoot
函数 之前就去注册了。)
import {allNativeEvents} from './EventRegistry';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
SimpleEventPlugin.registerEvents();
SimpleEventPlugin.registerEvents
函数:处理,加工原始事件名字为 React 事件名字,比如 将 click
变为 onClick
, 然后调用 registerSimpleEvent
函数 将原始事件名字和 React 事件名字 建立 Map 映射关系,比如 Map {click: onClick}
// 所有原生事件
const simpleEventPluginEvents = [
'abort',
'auxClick',
'click',
// 剩余所有原生事件
];
// React 事件和原始事件的映射 Map
export const topLevelEventsToReactNames = new Map();
SimpleEventPlugin.registerEvents
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);
}
}
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
第四:原始事件名字和 React 事件名字 建立 Map 映射关系 之后,在 registerSimpleEvent
函数 调用 registerTwoPhaseEvent
函数, 在 registerTwoPhaseEvent
函数中调用 registerDirectEvent
函数
结果是将所有原生事件名字 加入到 allNativeEvents 数组当中去,比如:[click, dbclick]
。调用两次是因为要在 registrationNameDependencies = {}
映射冒泡和捕获。比如:{onClick: click, onClickCapture: click}
为什么 dependencies 是数组,因为 一个 React 事件可能会对应多个原生事件。
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
// {onClick: click, onClickCapture: click}
export const registrationNameDependencies = {};
export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
) {
registrationNameDependencies[registrationName] = dependencies;
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
第五: 在项目当中的 index.tsx 中 调用 createRoot
函数
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(<App/>)
第六:在 createRoot
函数 当中 调用 listenToAllSupportedEvents
函数,并创建 FiberRoot
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks
);
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
第七:调用 listenToAllSupportedEvents
函数, 见名知意, 监听所有原生事件。 来看看这里是怎么监听。
经过前五步,已经将所有的原生事件都放到了 allNativeEvents 数组中。遍历 allNativeEvents 数组,调用 listenToNativeEvent
函数。
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
allNativeEvents.forEach(domEventName => {
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
}
第八:listenToNativeEvent 调用了 addTrappedEventListener
函数。
第九:addTrappedEventListener
函数 当中,首先通过 createEventListenerWrapperWithPriority
函数 创建了 listenr 监听函数,这个监听函数很重要,注意看!!!!
listenr 监听函数通过 dispatchEvent 去 bind 绑定一些重要参数,返回的一个函数。
这样每次事件触发都可以调用 dispatchEvent 并且携带一些固定的参数。
export function createEventListenerWrapper(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
return dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
然后调用 addEventBubbleListener
addEventCaptureListener
函数。
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
);
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
);
}
第十:调用 addEventBubbleListener
addEventCaptureListener
函数,真相大白 😀,原来结果就是在 root 根容器上绑定了原生事件的冒泡和捕获事件。
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
) {
target.addEventListener(eventType, listener, false);
retu电击 listener;
}
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}
Mout 阶段总结:点击之前 就是在 root 容器上监听了 JavaScript 所有原生事件的冒泡和捕获。
点击触发
总结:提取所有事件监听的处理函数放到
dispatchQueue
当中。然后,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。
上文提到,所有事件的触发,都绑定到了 dispachtEvent 函数上,相当于:
div.addEventListener("click", dispatchEvent.bind(null,
click,
eventSystemFlags,
div#root))
div.addEventListener("dbclick", dispatchEvent.bind(null,
dbclick,
eventSystemFlags,
div#root))
第一:点击触发,调用 dispatchEvent
函数。函数做了至关重要的两件事情。
第一件事情是:通过调用 extractEvents
函数, 提取所有监听的处理函数放到 dispatchQueue
当中。
第二件事情是:通过调用 processDispatchQueue
函数,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。
基于这两件事情,我们看看 Reect 是怎么完成的?
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
函数,首先根据不同的事件类型名字,去获取不同类别的事件对象 event,也就是React 传递给开发者的 事件对象 event。
然后,在该函数当中调用 accumulateSinglePhaseListeners
函数。这个 accumulateSinglePhaseListeners
函数 做的事情就是 从当前的 Fiebr 节点开始一直向上遍历,找到路径上 fiber 节点的所有绑定事件函数比如:onClick, onClickCapture。经过包装成为 Dispatch 返回给 listeners。
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent
);
然后将 Dispatch 和 匹配的 event 对象,封装成一个对象,加入到 dispatdchQueue
当中去,
dispatchQueue.push({ event, listeners });
extractEvents
函数整体实现
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget
): void {
const reactName = topLevelEventsToReactNames.get(domEventName);
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
// 首先根据不同的事件类型名字,去获取不同类别的事件对象 event,也就是React 传递给开发者的 事件对象 event。
switch (domEventName) {
case "click":
case "mousemove":
SyntheticEventCtor = SyntheticMouseEvent;
break;
case "drop":
SyntheticEventCtor = SyntheticDragEvent;
break;
default:
break;
}
// 通过调用 `extractEvents` 函数, 提取所有监听的处理函数放到 `dispatchQueue` 当中。
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent
);
if (listeners.length > 0) {
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
);
// Dispatch 和 匹配的 event 对象,封装成一个对象,加入到 `dispatdchQueue` 当中去,
dispatchQueue.push({ event, listeners });
}
}
第二件事情是:通过调用 processDispatchQueue
函数,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。比如:捕获阶段,从最高层节点向下传播,而加入到队列的顺序 是从目标节点开始向上加入的,所以要想模拟捕获,就需要从最后一个节点开始 倒序执行。要想模拟冒泡,就需要从 第一个节点开始,正序执行。
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];
// 这里的判断是因为,React 重写了 阻止冒泡的方法,可以通过 isPropagationStopped 来判断是否阻止
//了冒泡,如果阻止了冒泡,则立即返回。
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
// 执行事件监听函数,传入 Event 对象。
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
// 这里的判断是因为,React 重写了阻止冒泡的方法,可以通过 isPropagationStopped 来判断是否阻止
//了冒泡,如果阻止了冒泡,则立即返回。
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
// 执行事件监听函数,传入Event 对象。
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
总结:提取所有事件监听的处理函数放到
dispatchQueue
当中。然后,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。
至此,事件触发阶段完成