模拟实现
2024年01月25日
一、事件绑定
1.1 /packages/react-dom/root.js
在 React.js 18.x
的事件系统中,在 createRoot
会一口气向外层容器上注册绑定完全部事件。
import {
createContainer,
updateContainer
} from 'react-reconciler/fiberReconciler';
import { initEvent } from './syntheticEvent';
export function createRoot(container) {
const root = createContainer(container);
return {
render(element) {
initEvent(container, 'click');
return updateContainer(element, root);
}
};
}
1.2 /packages/react-dom/syntheticEvent.js
function dispatchEvent(container, eventType, e) {
const targetElement = e.target;
if (targetElement == null) {
console.log('事件不存在 target', e);
return;
}
const { bubble, capture } = collectPaths(targetElement, container, eventType);
const se = createSyntheticEvent(e);
triggerEventFlow(capture, se);
if (!se.__stopPropagation) {
triggerEventFlow(bubble, se);
}
}
export function initEvent(container, eventType) {
if (!validEventTypeList.includes(eventType)) {
console.log('不支持的事件类型', eventType);
return;
}
container.addEventListener(eventType, e => {
dispatchEvent(container, eventType, e);
});
}
本质上是向原生 DOM
注册绑定事件, 无论是通过 addEventCaptureListener
还是 addEventBubbleListener
, 都会将 dispatchEvent
作为真正的事件处理函数。
1.3 /packages/react-dom/hostConfig.js
在创建真实 DOM
时, 将 memoizedProps
或者 pendingProps
中的属性存储到真实 DOM
节点中。
export function createInstance(type, props) {
const element = document.createElement(type);
updateFiberProps(element, props);
return element;
}
1.4 /packages/react-dom/syntheticEvent.js
因此, Fiber
将 memoizedProps
或者 pendingProps
中的属性存储到真实 DOM
节点中。因此, 在后续事件收集的过程中, 只需要遍历查找 DOM
中的 elementPropsKey
属性即可。
export const elementPropsKey = '__props';
export function updateFiberProps(node, props) {
node[elementPropsKey] = props;
}
二、事件触发
2.1 /packages/react-dom/syntheticEvent.js dispatchEvent()
/**
* @description: dispatchEvent
* @param {*} container
* @param {*} eventType
* @param {*} e
* 1, 收集 targetDOM - containerDOM 之间所有的事件回调函数
* 2. 构造合成事件
* 3. 遍历 capture
* 4. 遍历 bubble
*/
function dispatchEvent(container, eventType, e) {
const targetElement = e.target;
if (targetElement == null) {
console.log('事件不存在 target', e);
return;
}
const { bubble, capture } = collectPaths(targetElement, container, eventType);
const se = createSyntheticEvent(e);
triggerEventFlow(capture, se);
if (!se.__stopPropagation) {
triggerEventFlow(bubble, se);
}
}
2.2 /packages/react-dom/syntheticEvent.js collectPaths()
function collectPaths(targetElement, container, eventType) {
const paths = {
bubble: [],
capture: []
};
while (targetElement && targetElement != container) {
const elementProps = targetElement[elementPropsKey];
if (elementProps) {
const callbackNameList = getEventCallbackNameFromEventType(eventType);
if (callbackNameList) {
callbackNameList.forEach((callbackName, i) => {
const eventCallback = elementProps[callbackName];
if (eventCallback) {
if (i === 0) {
paths.capture.unshift(eventCallback);
} else {
paths.bubble.push(eventCallback);
}
}
});
}
}
targetElement = targetElement.parentNode;
}
return paths;
}
2.3 /packages/react-dom/syntheticEvent.js
function createSyntheticEvent(e) {
const syntheticEvent = e;
syntheticEvent.__stopPropagation = false;
const originStopPropagation = e.stopPropagation;
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropagation = true;
if (originStopPropagation) {
originStopPropagation(e);
}
};
return syntheticEvent;
}
2.4 /packages/react-dom/syntheticEvent.js
function triggerEventFlow(paths, se) {
for (let i = 0; i < paths.length; i++) {
const callback = paths[i];
unstable_runWithPriority(eventTypeToSchedulerPriority(se.type), () => {
callback.call(null, se);
});
if (se.__stopPropagation) {
break;
}
}
}
三、效果测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Mini KaSong</title>
<script src="../dist/react.iife.js"></script>
</head>
<body>
<div id="root"></div>
<script>
const {
ReactDOM: { createRoot },
React: { createElement, useState, Fragment }
} = React;
function App() {
const [value, setValue] = useState(0);
const handleClick = () => {
setValue(value => value + 1);
setValue(value => value + 1);
setValue(value => value + 1);
};
return createElement(
'div',
{
onClick: handleClick
},
value
);
}
const root = createRoot(document.getElementById('root'));
root.render(createElement(App));
</script>
</body>
</html>