react的事件模型(部分源码)
前言
从v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中
无论是在document还是根 DOM 容器上监听事件, 都可以归为事件委托(代理)(mdn).
// container = v17.0.0 ? 根 DOM 容器 : document
container.addEventListener(eventType, (e: Event) => {
// react中大部分事件都会调用dispatchEvent(捕获/冒泡机制)
// 疑问:scroll, load等无捕获冒泡的事件呢?如何监听?
dispatchEvent(container, eventType, e);
});
何时监听container
- 在render时会初始化监听container
ReactDom.createRoot(document.querySelector("#root")).***render***(
<APP />
);
- 源码实现
export function createRoot(container: Container) {
const root = createContainer(container);
return {
render(element: ReactElementType) {
// 代理click以及其他事件
["click", "...", ...].forEach((eventType)=>{
container.addEventListener(eventType, (e: Event) => {
dispatchEvent(container, eventType, e);
});
})
// 省略...将element挂载到container,并开始进入调度/协调器,最终render出 dom页面
},
};
}
dispatchEvent做了一些什么
- 收集沿途的事件
当触发事件时,会获取到触发事件的targetElement,并会依赖collectPaths函数,从targetElement遍历到container,遍历过程中,分别将在协调期间赋值(何时赋值的:后文讲)在dom上面保存的react合成事件(例onClick等对应的回调函数…)分别以push和unshift到bubble(冒泡)数组和capture(捕获)数组
const { capture, bubble } = collectPaths(
targetElement,
container,
eventType
);
function collectPaths(
targetElement: DOMElement,
container: Container,
eventType: string
) {
const paths: Paths = {
capture: [],
bubble: [],
};
while (targetElement && targetElement !== container) {
// 收集的过程
const elementProps = targetElement[elementPropsKey];
if (elementProps) {
const callbackNameList =
getEventCallbackNameFormEventType(eventType);
if (callbackNameList) {
callbackNameList.forEach((callbackName, index) => {
const eventCallback = elementProps[callbackName];
if (eventCallback) {
if (index === 0) {
paths.capture.unshift(eventCallback);
} else {
paths.bubble.push(eventCallback);
}
}
});
}
}
targetElement = targetElement.parentElement as unknown as DOMElement;
}
return paths;
}
- 构造合成事件
添加一个属性**__stopPropagation ,用来标记是否要停止事件的传播。它初始值为false**。
接下来,函数会将原生事件对象的**stopPropagation方法赋值给合成事件的stopPropagation属性。在调用stopPropagation时,合成事件会将__stopPropagation标记为true ,并且如果原生事件对象也有stopPropagation**方法,则调用它。
function createSyntheticEvent(e: Event): SyntheticEvent {
const syntheticEvent = e as SyntheticEvent;
syntheticEvent.__stopPropagation = false;
const originStopPropagation = e.stopPropagation;
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropagation = true;
if (originStopPropagation) {
originStopPropagation();
}
};
return syntheticEvent;
}
- 遍历capture 捕获
- 遍历bubble 冒泡
// 3. 遍历capture 捕获
triggerEventFlow(capture, se);
if (!se.__stopPropagation) {
// 4. 遍历bubble 冒泡
triggerEventFlow(bubble, se);
}
triggerEventFlow实现
function triggerEventFlow(paths: EventCallback[], se: SyntheticEvent) {
for (let i = 0; i < paths.length; i++) {
const callback = paths[i];
callback.call(null, se);
if (se.__stopPropagation) {
break;
}
}
}
onClick等对应的回调函数是如何合适挂载到dom上的
-
创建dom时
在completeWork阶段(递归的’归‘阶段)处理tag为HostComponent且是mount阶段会创建dom
- 在
fiber的tag为HostComponent中会调用宿主包(reactDom)的createInstance,在此时非常适合将props中的合成事件(onClick等…)保存在dom上(调用updateFiberProps)
export function updateFiberProps(node: DOMElement, props: Props) { node[elementPropsKey] = props; } - 在
-
更新属性时
在completeWork阶段(递归的’归‘阶段)处理HostComponent且是update阶段会更新属性
update阶段
Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props当props有属性有变化会标记update的flag,在commit阶段再去调用
updateFiberProps思考重点
宿主环境单独实现合成事件,需要和react解耦,合成事件相关事件函数需要在宿主包实现,react core只实现挂载合成事件到真实dom(具体挂载依然是调用宿主环境包对应函数)