react事件模型源码学习分享

155 阅读3分钟

react的事件模型(部分源码)

前言

v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中

image.png

无论是在document还是根 DOM 容器上监听事件, 都可以归为事件委托(代理)(mdn).

// container = v17.0.0 ? 根 DOM 容器 :  document
container.addEventListener(eventType, (e: Event) => {
    // react中大部分事件都会调用dispatchEvent(捕获/冒泡机制)
    // 疑问:scroll, load等无捕获冒泡的事件呢?如何监听?
    dispatchEvent(container, eventType, e);
});

何时监听container

  1. 在render时会初始化监听container
ReactDom.createRoot(document.querySelector("#root")).***render***(
    <APP />
);
  1. 源码实现
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做了一些什么

  1. 收集沿途的事件

当触发事件时,会获取到触发事件的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;
}
  1. 构造合成事件

添加一个属性**__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;
}
  1. 遍历capture 捕获
  2. 遍历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上的

  1. 创建dom时

    在completeWork阶段(递归的’归‘阶段)处理tag为HostComponent且是mount阶段会创建dom

    1. fibertagHostComponent中会调用宿主包(reactDom)的createInstance,在此时非常适合将props中的合成事件(onClick等…)保存在dom上(调用updateFiberProps)
    export function updateFiberProps(node: DOMElement, props: Props) {
        node[elementPropsKey] = props;
    }
    
  2. 更新属性时

    在completeWork阶段(递归的’归‘阶段)处理HostComponent且是update阶段会更新属性

    update阶段 Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props

    当props有属性有变化会标记update的flag,在commit阶段再去调用updateFiberProps

    思考重点

    宿主环境单独实现合成事件,需要和react解耦,合成事件相关事件函数需要在宿主包实现,react core只实现挂载合成事件到真实dom(具体挂载依然是调用宿主环境包对应函数)