首先定义入口文件,使用函数组件。
function FunctionComponent() {
return (
<h1 onClick={() => console.log("父冒泡")} onClickCapture={() => console.log("父捕获")}>
<span onClick={() => console.log("子冒泡")} onClickCapture={() => console.log("子捕获")}>
test
</span>
</h1>
);
}
const element = <FunctionComponent />;
const root = createRoot(document.getElementById("root"));
root.render(element);
简单处理函数组件
要处理函数组件,需要根据 Fiber 节点 tag 进行处理。在 beginWork 函数中做判断。
export function beginWork(current, workInProgress) {
switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(current, workInProgress, workInProgress.type);
//...
}
}
主要逻辑在 mountIndeterminateComponent 中。
function mountIndeterminateComponent(current, workInProgress, Component) {
console.log("挂载函数组件");
const props = workInProgress.pendingProps;
// const value = Component(props);
const value = renderWithHooks(current, workInProgress, Component, props);
workInProgress.tag = FunctionComponent;
reconcileChildren(current, workInProgress, value);
return workInProgress.child;
}
对于当前阶段,先简单处理一下。单独定义 renderWithHooks 函数方便以后扩展。
export function renderWithHooks(current, workInProgress, Component, props) {
const children = Component(props);
return children;
}
这样就可以了,对于函数组件的简单渲染。
合成事件
在一开始的入口文件中,定义了一个函数式组件,其中返回了两个元素,这两个元素分别绑定了捕获和冒泡事件。分别为:
- 父捕获
- 子捕获
- 子冒泡
- 父冒泡 对于 React 的期望来说,需要做的就是将定义的回调函数按照原生事件执行顺序执行,也就是上面列的顺序。
需要实现这个模型,在 react 中通过事件代理实现。
实现合成事件的核心文件是 react-dom-bindings/src/events/DOMPluginEventSystem.js。该文件导出 listenToAllSupportedEvents 函数,并且在 createRoot 函数中进行调用。
准备阶段
在准备阶段,需要记录需要处理哪些事件,比如 click,drop 等等。
入口在 react-dom-bindings/src/events/DOMPluginEventSystem.js。
在这个文件中会调用 SimpleEventPlugin.registerEvents 函数,SimpleEventPlugin 是一个插件,位于 react-dom-bindings/src/events/plugins/SimpleEventPlugin.js。
对于 registerEvents 函数,这个插件其并没有做处理,只是做了一层转包。
// 删除其他不相干代码
import { registerSimpleEvents } from "../DOMEventProperties";
export { registerSimpleEvents as registerEvents };
可以看到,registerEvents 函数其实是来自 DOMEventProperties 文件导出的 registerSimpleEvents 函数。
import { registerTwoPhaseEvent } from "./EventRegistry";
const simpleEventPluginEvents = ["click"];
export const topLevelEventToReactNames = new Map();
function registerSimpleEvent(domEventName, reactName) {
topLevelEventToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = simpleEventPluginEvents[i];
const domEventName = eventName.toLowerCase();
const capitalizeEventName = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, `on${capitalizeEventName}`);
}
}
这个文件中其实就干了两件事。
- 处理
topLevelEventToReactNames - 调用
registerTwoPhaseEvent函数
topLevelEventToReactNames 变量事一个 map 键值对。key 为原生事件名,value 为 react 事件名。
例如 click - onClick。
// 由于 registerTwoPhaseEvent 逻辑相对简单,就放在这里自行阅读。
export const allNativeEvents = new Set();
/**
* 注册两个阶段的事件
* @param {*} registrationName React 事件名 onClick
* @param {*} dependencies 原生事件数组 ['click']
*/
export function registerTwoPhaseEvent(registrationName, dependencies) {
// 注册冒泡事件的对应关系
registerDirectEvent(registrationName, dependencies);
// 注册捕获事件的对应关系
registerDirectEvent(registrationName + "Capture", dependencies);
}
export function registerDirectEvent(registrationName, dependencies) {
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
以上,就是准备阶段的工作。
挂载阶段
一开始说过,合成事件是通过事件代理模拟原生事件执行逻辑。事件代理需要将事件处理器绑定到目标事件触发对象的父级元素上,在 react 项目中,有一个根元素满足这个条件,那就是 div#root。所以需要将事件处理器绑定到这个元素上。
对应到代码中则是在 creteRoot 函数中调用的 listenToAllSupportedEvents 函数。
const listeningMarker = "__reactListening" + Math.random().toString(36).slice(2);
export function listenToAllSupportedEvents(rootContainerElement) {
// 监听所有事件,只监听一次
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach((domEventName) => {
listenToNativeEvent(domEventName, true, rootContainerElement);
listenToNativeEvent(domEventName, false, rootContainerElement);
});
}
}
这个函数接收一个 rootContainerElement 参数,那这个参数是什么呢?
这个参数其实就是 div#root 元素。上面说过,这个函数是在 creteRoot 函数中调用的。在下面的代码中可以看到,调用 listenToAllSupportedEvents 函数时传入了 container 参数。
同时 creteRoot 函数是在 main.ts 入口文件调用的,传入的参数就是 div#root 元素。
export function createRoot(container) {
const root = createContainer(container);
listenToAllSupportedEvents(container);
return new ReactDOMRoot(root);
}
在 listenToAllSupportedEvents 函数中使用了一个变量 listeningMarker 当作 rootContainerElement 的 key。并赋值为 true,以作一个判断,当已经赋值过,表示已经挂载过了,就不用再次挂载。变量 listeningMarker是一个随机值。
同时使用了变量 allNativeEvents。这是在 registerTwoPhaseEvent 函数中进行处理,位于 react-dom-bindings/src/events/EventRegistry.js。这是一个 Set 变量,存储所有需要监听的原生事件。例如 click 事件。
接下来就是遍历这个 Set 对其中的每个事件进行监听。即调用 listenToNativeEvent 函数。
/**
* 注册原生事件
* @param {*} domEventName 事件名
* @param {*} isCapturePhaseListener 是否是捕获事件
* @param {*} target 目标DOM 节点
*/
export function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(domEventName, isCapturePhaseListener, target, eventSystemFlags);
}
在这个函数中,使用了两个变量,eventSystemFlags 和 IS_CAPTURE_PHASE。这两个变量是二进制数字,表示是否为捕获阶段。然后调用 addTrappedEventListener 函数。
function addTrappedEventListener(domEventName, isCapturePhaseListener, targetContainer, eventSystemFlags) {
const listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
if (isCapturePhaseListener) {
addEventCaptureListener(targetContainer, domEventName, listener);
} else {
addEventBubleListener(targetContainer, domEventName, listener);
}
}
这个函数逻辑也相对简单,首先创建一个 listener,这个就是事件处理器,也就是回掉函数,接下来根据事件是否为捕获调用不同的挂载函数。这两个函数就是执行原生挂载,将生成的 listener 挂载到对应的原生事件回掉函数上。
export function addEventCaptureListener(target, eventType, listener) {
target.addEventListener(eventType, listener, true);
return listener;
}
export function addEventBubleListener(target, eventType, listener) {
target.addEventListener(eventType, listener, false);
return listener;
}
至此,挂载阶段完成了,接下来需要看执行阶段。
执行阶段
执行阶段,其实就是用户点击元素后触发事件,执行回调函数的阶段。这个时候执行的回调函数其实就是上面的 listener。所以,接下来需要搞清楚这个 listener 是如何被创建的。
export function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
const listenerWrapper = dispatchDiscreteEvents;
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
根据代码可知,createEventListenerWrapperWithPriority 返回了一个新的函数,这个新函数其实是绑定了 this 为 null 且绑定了 domEventName, eventSystemFlags, targetContainer 三个参数的
dispatchDiscreteEvents 函数。
于是可以下一个结论,在用户点击时,就是执行的 dispatchDiscreteEvents 函数。
/**
* 派发离散事件的监听函数
* @param {*} domEventName 事件名
* @param {*} eventSystemFlags 系统标志 冒泡或捕获
* @param {*} container 容器
* @param {*} nativeEvent 原生事件
*/
function dispatchDiscreteEvents(domEventName, eventSystemFlags, container, nativeEvent) {
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
}
function dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent) {
// 获取事件源
const nativeEventTarget = getEventTarget(nativeEvent);
// 获取对应 Fiber 实例
const targetInst = getClosestInstanceFromNode(nativeEventTarget);
dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, container);
}
由以上代码可以看到,dispatchDiscreteEvents 其实接收 4 个参数,其中前三个在 createEventListenerWrapperWithPriority 函数中已经被绑定,第四个 nativeEvent 其实是由 addEventListener 传入。即原生事件对象。
dispatchDiscreteEvents 会将这些参数透传给 dispatchEvent 函数。
dispatchEvent 首先会获取事件源。
export default function getEventTarget(nativeEvent) {
return nativeEvent.target || nativeEvent.srcElement || window;
}
然后会获取对应的 fiber 节点。这里可以根据之前缓存的数据直接获取。
/**
* 从真实DOM 节点上获取对应的 fiber
* @param {*} targetNode
* @returns
*/
export function getClosestInstanceFromNode(targetNode) {
const targetInst = targetNode[internalInstanceKey];
return targetInst;
}
/**
* 提前缓存 Fiber 节点实例到 DOM 节点,在创建真实 DOM 时执行
* @param {*} hostInst
* @param {*} node
*/
export function precacheFiberNode(hostInst, node) {
node[internalInstanceKey] = hostInst;
}
最后执行 dispatchEventForPluginEventSystem 函数。
export function dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
dispatchEventForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer);
}
function dispatchEventForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
const nativeEventTarget = getEventTarget(nativeEvent);
// 派发事件的数组
const dispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
dispatchEventForPluginEventSystem 函数只是调用了 dispatchEventForPlugins 函数并透传参数。
dispatchEventForPlugins 函数第一件事就是维护一个事件队列。
对于要实现原生事件执行顺序的需求来说,根据一开始的执行顺序图,可以想象,如果拿到从出发点事件元素到最外层元素的所有事件处理函数,并放到一个队列中,那么对于捕获事件来说,就是反着执行一遍,对于冒泡事件来说,就是正着执行一遍。
extractEvents 函数就是对 dispatchQueue 进行处理。
function extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
}
/**
* 把要执行的回掉函数添加到 dispatchQueue 中
* @param {*} dispatchQueue 派发队列,里面放置回掉函数
* @param {*} domEventName DOM 事件名称
* @param {*} targetInst 目标 fiber
* @param {*} nativeEvent 原生事件
* @param {*} nativeEventTarget 原生事件源
* @param {*} eventSystemFlags 系统标识
* @param {*} targetContainer 目标容器
*/
function extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
) {
const reactName = topLevelEventToReactNames.get(domEventName);
// 合成事件构造函数
let SyntheticEventCtor;
switch (domEventName) {
case "click":
SyntheticEventCtor = SyntheticMouseEvent;
break;
default:
break;
}
const isCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
const listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, isCapturePhase);
if (listeners.length > 0) {
const event = new SyntheticEventCtor(reactName, domEventName, targetInst, nativeEvent, nativeEvent.target);
dispatchQueue.push({ event, listeners });
}
}
extractEvents 函数调用了 SimpleEventPlugin 中导出的 extractEvents 函数。
extractEvents 函数一开始根据原生事件名获取对应的 react 事件名。
再根据 react 事件名选取对应的合成事件构造函数,并赋值给 SyntheticEventCtor 变量。
再根据 eventSystemFlags 标识判断是否为捕获。
然后会调用 accumulateSinglePhaseListeners 函数获取所有 react 事件绑定的函数。
export function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, isCapturePhase) {
const captureName = reactName + "Capture";
const reactEventName = isCapturePhase ? captureName : reactName;
const listeners = [];
let instance = targetFiber;
while (instance !== null) {
const { stateNode, tag } = instance;
if (tag === HostComponent && stateNode !== null) {
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(createDispatchListener(instance, listener, stateNode));
}
}
instance = instance.return;
}
return listeners;
}
accumulateSinglePhaseListeners 函数的逻辑就是从当前节点开始,逐层向上查找,获取每个元素的 listener。
接下来就是在存在 listeners 的情况下,创建 SyntheticEventCtor 实例,这是合成事件类。然后推入 dispatchQueue。
import assign from "shared/assign";
function functionThatReturnsTrue() {
return true;
}
function functionThatReturnsFalse() {
return false;
}
const MouseEventInterface = {
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
pageX: 0,
pageY: 0,
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
getModifierState: function (key) {},
button: 0,
buttons: 0,
};
function createSyntheticEvent(inter) {
/**
* 合成事件的基类
* @param {*} reactName React 属性名 onClick
* @param {*} reactEventType click
* @param {*} targetInst 事件源对应的 fiber 实例
* @param {*} nativeEvent 原生事件对象
* @param {*} nativeEventTarget 原生事件源对应的真实DOM
*/
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
this._reactName = reactName;
this.type = reactEventType;
this._targetInst = targetInst;
this._nativeEvent = nativeEvent;
this.target = nativeEventTarget;
for (const propName in inter) {
if (!inter.hasOwnProperty(propName)) {
continue;
}
this[propName] = nativeEvent[propName];
}
// 是否已经阻止默认行为
this.isDefaultPrevented = functionThatReturnsFalse;
// 是否阻止继续传播
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
assign(SyntheticBaseEvent.prototype, {
preventDefault() {
const event = this.nativeEvent;
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
stopPropagation() {
const event = this.nativeEvent;
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
});
return SyntheticBaseEvent;
}
export const SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface);
这里有一个合成事件基类。通过给 createSyntheticEvent 函数传入不同的参数生成不同的合成事件类。这个函数主要是绑定属性到 react 事件和原型链上,包括原生属性,和一些内置属性,还有两个方法。
接下来就可以去执行了。
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
// 判断是否在捕获阶段
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0; i < dispatchQueue.length; i++) {
const { event, listeners } = dispatchQueue[i];
processDispatchQueueItemInOrder(event, listeners, inCapturePhase);
}
}
function executeDispatch(event, listener, currentTarget) {
event.currentTarget = currentTarget;
listener(event);
}
function processDispatchQueueItemInOrder(event, listeners, inCapturePhase) {
if (inCapturePhase) {
for (let i = listeners.length - 1; i >= 0; i--) {
const { listener, currentTarget } = listeners[i];
if (event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
}
} else {
for (let i = 0; i < listeners.length; i++) {
const { listener, currentTarget } = listeners[i];
if (event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
}
}
}
核心逻辑是遍历 dispatchQueue,拿到 react 事件实例和对应的 listeners。然后根据是否是捕获来正向或者反向执行。捕获反向执行,冒泡正向执行。listeners 中每个 listener 都是 react 事件的回调函数,在执行时将 react 事件实例传入即可。
结束
以上只针对 click 事件进行举例说明。