跳到主要内容

模拟实现

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

因此, FibermemoizedProps 或者 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>