likes
comments
collection
share

React源码系列(四)------ 事件系统

作者站长头像
站长
· 阅读数 17

前言

本文讲的是React的事件系统,React事件系统并不难,主要就是采用了事件代理的方式,我们先来讲下基础知识。

基础知识

DOM事件流

事件流的三个阶段

  • 事件捕获阶段
  • 目标阶段
  • 事件冒泡阶段

触发顺序:捕获 -> 目标 -> 冒泡。

React源码系列(四)------ 事件系统

像这张图,假设用户在App触发了事件,整个事件流的触发顺序是:

document -> body -> div#root -> App(目标) -> div#Root -> body -> document。

绑定事件

绑定事件时,我们可以选择是在捕获阶段还是冒泡阶段绑定处理函数,在addEventListener中的第三个参数,如果为true则是捕获阶段,如果为false则为冒泡阶段。

element.addEventListener(event, function, useCapture);

阻止冒泡

如果想要在冒泡阶段只触发目标的处理函数,可以通过阻止冒泡阻断事件的传播。

element.addEventListener(event, (e) => e.stopPropagation(), useCapture);

事件代理

事件代理就是将原本应该绑在子元素上的事件改成绑在父元素上,让父元素监听这个事件。这种方式主要就是依赖冒泡机制,任何一个子元素触发事件都能执行父元素上绑定的那个事件处理函数。

主要的优点是,这种方式大大减少了事件的绑定,只需给父元素绑定一次,而且无论后续是否增加子元素都不需要绑定事件,节省了内存占用。

这里已经铺垫完了前置知识,接下来我们来看看React事件系统到底是什么样的。

React事件系统

React事件系统主要原理就是事件代理,只给div#root绑定事件,然后所有div#root内的元素触发事件都是通过冒泡机制,一直冒到div#root上,然后再触发事件处理函数的。

整个事件系统其实主要分为两个阶段,一个是事件注册,一个是事件派发。

事件注册

相信很多React使用者都很好奇,为什么在React中注册事件使用的是小驼峰。比如onclick在React中要写成onClick,这两个东西使用起来那么像?他们是一样的吗?其实他们是两个东西,诸如onClick、onChange、onMouseEnter等,他们都是React 提前定死的,然后绑定他们和原生的onclick、change、mouseenter之间的关系,而处理他们之间的关系就是事件注册这个阶段来完成的。

我们来看看源码中是如何注册的。

收集事件,处理事件对应关系

首先先写好要注册的事件名。源码中远不止这点事件,除了simpleEventPluginEvents,还有nonDelegatedEvents等等,就不一一举例了。

const simpleEventPluginEvents = ['abort','auxClick','cancel','canPlay','canPlayThrough','click','close','contextMenu','copy','cut','drag','dragEnd','dragEnter','dragExit','dragLeave','dragOver','dragStart','drop','durationChange','emptied','encrypted','ended','error','gotPointerCapture','input','invalid','keyDown','keyPress','keyUp','load','loadedData','loadedMetadata','loadStart','lostPointerCapture','mouseDown','mouseMove','mouseOut','mouseOver','mouseUp','paste','pause','play','playing','pointerCancel','pointerDown','pointerMove','pointerOut','pointerOver','pointerUp','progress','rateChange','reset','resize','seeked','seeking','stalled','submit','suspend','timeUpdate','touchCancel','touchEnd','touchStart','volumeChange','scroll','toggle','touchMove','waiting','wheel',];

然后通过registerSimpleEvents给他们绑上类似于onClick就是click这样的关系。

function registerSimpleEvents() {
    for (let i = 0; i < simpleEventPluginEvents.length; i++) {
        const eventName = simpleEventPluginEvents[i]; // mouseMove
        const domEventName = eventName.toLowerCase(); // mouseMove
        const capitalizeEvent = eventName[0].toUpperCase() + eventName.slice(1); // MouseMove
        // 下面这个函数的具体作用往下看
        registerSimpleEvent(domEventName, `on${capitalizeEvent}`); // mouseMove onMouseMove
    }
};

然后将这些绑好关系的事件名都放到一个Map上去,用于以后的派发事件。

const topLevelEventsToReactNames = new Map();
function registerSimpleEvent(domEventName, reactName) {
    topLevelEventsToReactNames.set(domEventName, reactName);
    // 这个函数的具体作用往下看
    registerTwoPhaseEvent(reactName, [domEventName]);
}

最后是将上面这些事件分成冒泡和捕获两种,放入到一个Set里面,这个Set就是需要给div#root绑定的事件,到此就是注册事件的收集绑定事件的全过程了。

const allNativeEvents = new Set();
function registerDirectEvent(_, dependencies) {
    for (let i = 0; i < dependencies.length; i++) {
        allNativeEvents.add(dependencies[i]); // click
    }
}
function registerTwoPhaseEvent(registrationName, dependencies) {
    // 注册冒泡事件的对应关系
    registerDirectEvent(registrationName, dependencies);
    // 注册捕获事件的对应的关系
    registerDirectEvent(registrationName + 'Capture', dependencies);
}

ps: 以上内容的处理的时机是非常早的,我们只需要知道它早于createRoot就好了。

给div#root绑定事件

在createRoot时,会进行一次事件的绑定。

const ListeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
unction listenToAllSupportedEvent(divRoot) {
    // 监听根容器,div#root只监听一次
    if (!divRoot[ListeningMarker]) {
        divRoot[ListeningMarker] = true;
        // 遍历所有原生事件比如click,进行监听,这里的listener就是派发事件。
        allNativeEvents.forEach((domEventName) => {
            divRoot.addEventListener(domEventName, listener, true);
            divRoot.addEventListener(domEventName, listener, false);
        })
    }
}

到此就是整个注册事件的全流程了,以下是一张流程图。

React源码系列(四)------ 事件系统

事件派发

前置知识

每个fiber对应的真实DOM都会有一个属性,这个属性会指向自己的fiber(这个事情是在completeWork阶段做的)。

const randomKey = Math.random().toString(36).slice(2);
const internalInstanceKey = '__reactFiber$' + randomKey;
// node就是真实DOM
function updateFiberProps(node, props) {
    node[internalPropsKey] = props;
}

主流程

以下均以onClick举例,当说到onClick时,其实是通用到所有其他事件上的。

所谓的派发事件,就是当拥有onClick属性的子元素触发点击事件时,通过冒泡,直到div#root,此时div#root会从事件对象中获取到目标元素的真实DOM,再通过此真实DOM获取对应的fiber,最后通过fiber树获取到目标元素的所有父fiber,将这些父fiber身上的onClick属性值都按顺序收集到一个数组中,然后按照顺序逐个调用他们。

React源码系列(四)------ 事件系统

其实到这整个流程就已经讲得非常清晰了,但在流程图中出现了一个叫合成事件的构造函数(SyntheticEventCtor),这是个什么东西?

我们来思考一下,假设我们在目标元素的事件里写了阻止冒泡,react该怎么处理?在整个事件派发的流程中,我们是必须让事件冒泡到div#root上的,如果直接阻止冒泡,那整个事件都将无法执行。

为了解决以上这个问题,react对原生的事件对象进行了封装(封装后叫做合成事件对象),然后传给onClick调用的那个处理函数,也就是说我们在onClick中调用的事件对象其实是react的合成事件对象,并非原生事件对象。在这个合成事件对象中有一个很关键的地方,那就是改写了stopPropagation。我们来看下源码大概是怎样的。

function createSyntheticEvent(inter) {
    /**
     * 合成事件的基类
     * @param {any} nativeEvent 原生事件对象
     */
    function SyntheticBaseEvent(nativeEvent) {
        this.nativeEvent = nativeEvent;
        // 把属性从原生事件上拷贝到合成事件实例上
        for (const propName in nativeEvent) {
            this[propName] = nativeEvent[propName];
        }
        // 是否已经阻止继续传播
        this.isPropagationStopped = () => true;
        assign(SyntheticBaseEvent.prototype, {
            stopPropagation() {
                const event = this.nativeEvent;
                if (event.stopPropagation) {
                    event.stopPropagation();
                } else {
                    event.cancelBubble = false;
                }
                this.isPropagationStopped = () => true;
            },
        })
        return this;
    };
    return SyntheticBaseEvent;
};

以上代码可以看出,这个合成事件对象上有一个isPropagationStopped成员,如果这个成员为true,我们就不继续需执行下去了。

React源码系列(四)------ 事件系统

结尾

以上就是事件系统的全部内容了,主要流程可以总结为以下几步:

  • 给div#root绑定事件
  • 触发事件时通过冒泡机制触发div#root的事件处理
  • 通过fiber树收集目标元素以及目标元素的父元素的事件处理函数
  • 通过正序或逆序的方式触发收集到的事件处理函数模拟出捕获或冒泡

最后就是老规矩,附上本文的代码以及整个react18.2的代码供各位调试。

event代码:点这里

源码:点这里

上一篇:commitRoot