likes
comments
collection
share

一文帮你熟悉 React17 事件机制 (二)

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

触发事件: 事件收集 & 处理事件

为了方便理解, 我们介绍下面事件触发, 用click 事件举例子, 当页面中有个button 按钮, 我们触发按钮点击。

一文帮你熟悉 React17 事件机制 (二) 流程如下:

  1. 当点击 button 按钮时候, 当前事件流是捕获阶段, windowdiv#root 事件委托节点, 由于在此绑定事件捕获监听, 在此节点触发 click 捕获事件函数触发, 执行React 事件执行处理机制 (这里后面讲解)
  2. 事件处理完之后, 继续捕获, 一直到目标阶段, 找到 button 元素, 由于容器节点下是没有任何事件的, 下来执行事件流冒泡阶段
  3. button 元素一直往上找, 当到div#root 事件委托节点时候, 由于在此绑定事件冒泡监听, 在此节点触发 click 冒泡事件函数触发, 执行React 事件处理 (这里后面讲解)
  4. 事件处理完之后, 继续冒泡, 一直到window, 中间如果还有事件, 执行事件
  5. React事件处理还是基于 js事件流处理流程, 在委托节点 捕获 和 冒泡 处理里面执行自己逻辑, 即React 事件执行处理
  6. 遇到阻止事件传播执行其逻辑: React里面使用 e.stopPropagation() 阻止事件传播

阻止事件传播 流程如下:

  • 捕获阶段 & 冒泡阶段 遵循 JS事件机制
  • React 事件处理 由于委托在 容器节点上, 在容器节点元素这里 阻止后续 JS 事件机制执行
  • React 事件系统 内部 虚拟冒泡收集事件队列, 执行队列时候阻止后续事件执行

下面是一个 触发不同位置 阻止事件传播 例子 点击内容: 分别在 位置 1, 2, 3, 4 触发阻止事件传播。

<div id="container"></div>
<script>
    function Content() {
        React.useEffect(() => {
            const id = document.getElementById('ele-id');
            const handleDocumentClick = (e) => {
                console.log('[原生] click @document');
            };
            const handleDocumentClickCapture = (e) => {
                console.log('[原生] click @document (capture 阶段)');
            };
            const handleClick = (e) => {
                console.log('[原生] click $div#ele-id 元素');
                //  位置3:  e.stopPropagation();
            };
            const handleClickCapture = (e) => {
                console.log('[原生] click $div#ele-id 元素 (capture 阶段)');
                // 位置4:  e.stopPropagation();
            };
            document.addEventListener('click', handleDocumentClick);
            document.addEventListener('click', handleDocumentClickCapture, true);
            id.addEventListener('click', handleClick);
            id.addEventListener('click', handleClickCapture, true);
            return () => {
                document.removeEventListener('click', handleDocumentClick);
                id.removeEventListener('click', handleClick);
                id.removeEventListener('click', handleClickCapture, true);
            };
        }, []);
        return (
            <div
                 onClick={(e) => {
                     console.log('[React] click @Content 组件');
                     // 位置1:  e.stopPropagation();
                 }}
                 onClickCapture={(e) => {
                     console.log('[React] click @Content 组件 (capture 阶段)');
                     // 位置2:  e.stopPropagation();
                 }}
             >
                <div id="ele-id">
                    <button
                        onClick={() => {
                            console.log('[React] click $button 元素');
                        }}
                    >
                        {'点击内容'}
                    </button>
                </div>
             </div>
        );
    }
    function App() {
        return (
            <div
                 onClick={(e) => {
                     console.log('[React] click @App 组件');
                 }}
                 onClickCapture={(e) => {
                     console.log('[React] click @App 组件 (capture 阶段)');
                 }}
             >
                <Content />
             </div>
        );
    }
    ReactDOM.render(
        <App />,
        document.getElementById('container'), () => {}
    );
</script>
  • 不添加任何 阻止事件传播
[原生] click @document (Capture 阶段)
[React] click @App 组件
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)
[原生] click $div#ele-id 元素
[React] click $button 元素
[React] click @Content 组件
[React] click @App 组件
[原生] click @document
  • 位置1: [React] Content 组件 onClick 添加 e.stopPropagation()

    • 阻止冒泡阶段 组件Content真实事件 后的事件执行
    • 委托给 容器, 阻止位置在div#container 后面 冒泡执行: document (即 body -> document -> window)
    • div#ele-id 元素绑定监听原生事件, 冒泡先冒泡此元素, 因此会先执行
[原生] click @document (capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)
[原生] click $div#ele-id 元素
[React] click $button 元素
[React] click @Content 组件
  • 位置2: [React] Content 组件 onClickCapture 添加 e.stopPropagation()
    • 阻止捕获阶段 组件Content React事件 后的事件执行, 后续React捕获事件被阻止
    • 委托给 容器, 阻止位置在div#container 后面 捕获 + 冒泡 都被阻止
[原生] click @document (Capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
  • 位置3: [原生] $div#ele-id 元素 click冒泡 添加 e.stopPropagation()
    • 阻止冒泡阶段 div#ele-id 元素 后的冒泡事件传播
      • 委托给 容器, 阻止位置在div#container, div#ele-id 冒泡阶段的 后续都被阻止
[原生] click @document (Capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)
[原生] click $div#ele-id 元素
  • 位置4: [原生] $div#ele-id 元素 click捕获 添加 e.stopPropagation()
    • 阻止捕获阶段 div#ele-id 元素 后的捕获 + 冒泡事件传播
    • 委托给 容器, div#containerdiv#ele-id 捕获阶段的前面, 因此 React 捕获事件 被执行
[原生] click @document (Capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)

React 事件处理

点击元素, 根据 捕获阶段 和 冒泡阶段 处理委托节点对应函数执行, 那么函数执行中 React做了什么呢? 我们接下来揭秘 执行React 事件处理

下图是React 事件触发 流程图, 经历 捕获阶段 -> 冒泡阶段, 处理 React 事件 和 原生事件的基本流程:

一文帮你熟悉 React17 事件机制 (二)

执行委托处理函数: React 虚拟冒泡收集 React 处理事件, 收集之后执行函数

前面讲到, 我们根据事件优先级委托绑定事件监听器有三种, 分别为

  • 离散事件监听器(dispatchDiscreteEvent)
  • 用户阻塞事件监听器(dispatchUserBlockingUpdate)
  • 连续事件及其他事件监听器(dispatchEvent)

离散事件 和 用户阻塞事件监听器 最终还是调用 dispatchEvent我们简单看下 dispatchDiscreteEvent 和 dispatchUserBlockingUpdate

用户阻塞事件监听器: 第二类 dispatchUserBlockingUpdate (非重点)
/*
* 用户阻塞事件监听器:  简化版本
* 前三个参数是在注册事件代理的时候便传入的
*     domEventName:对应原生事件名称
*     eventSystemFlags:本文范文内其值仅有可能为 4 或者 0,分别代表 捕获阶段事件 和 冒泡阶段事件
*     container:应用根 DOM 节点
*     nativeEvent:原生监听器传入的 Event 对象
*/
function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent) {
    /**
     * runWithPriority => Scheduler_runWithPriority => Scheduler.unstable_runWithPriority
     * UserBlockingPriority 任务优先级 2
     * dispatchEvent.bind(...): 执行的任务
     *   - 前三个参数是在注册事件代理的时候便传入的,
            domEventName:对应原生事件名称
            eventSystemFlags:这里执行其值仅有可能为 4 或者 0,分别代表 捕获阶段事件 和 冒泡阶段事件
            container:应用根 DOM 节点
            nativeEvent:原生监听器传入的 Event 对象
        
        runWithPriority 里面会执行 第二个函数
        runWithPriority(priority, fn) {
           fn();
        }
     */
     runWithPriority(
         UserBlockingPriority, // 2
         dispatchEvent.bind(null, domEventName, eventSystemFlags, container, nativeEvent)
     );
}
  • 调用 unstable_runWithPriority 调度函数, 优先级不够时候阻止事件运行, 否则直接运行 dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)

runWithPriority 处理代码

function runWithPriority(reactPriorityLevel, fn) {
    // React优先级 获取 调度优先级 
    var priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
    // unstable_runWithPriority
    return Scheduler_runWithPriority(priorityLevel, fn);
}
var Scheduler_runWithPriority = Scheduler.unstable_runWithPriority;
/**
* 调度 执行函数:  
*    - 优先级不够时候, 直接不执行
*    - 否则 直接运行 函数
*/
function unstable_runWithPriority(priorityLevel, eventHandler) {
    switch (priorityLevel) {
      case ImmediatePriority:
      case UserBlockingPriority:
      case NormalPriority:
      case LowPriority:
      case IdlePriority:
        break;
      default:
        priorityLevel = NormalPriority;
    }
    // ...
    try {
        return eventHandler();
    } finally {
        // ...
    }
}
离散事件监听器 : 第一类 dispatchDiscreteEvent (非重点)
// 离散事件监听器 简化版本
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
    // ...
    // 新建一个离散更新
    // 实际: 后面四个参数实际上第一个函数的参数 
    //     - dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
    discreteUpdates(
        dispatchEvent,
        domEventName,
        eventSystemFlags,
        container,
        nativeEvent
    );
}
  
function discreteUpdates(fn, a, b, c, d) {
    // ...
    try {
        // 调用 Scheduler 里的离散更新函数: 看 discreteUpdates$1 => unstable_runWithPriority
        // 最终和  dispatchUserBlockingUpdate 调用一致
        return discreteUpdatesImpl(fn, a, b, c, d);
    } finally {
        // ...
    }
}
  • 离散事件监听器 执行的是 discreteUpdatesImpl 函数
    • discreteUpdatesImpl 经过一系列函数处理, 最终执行的是 fn(a, b, c, d)
    • fn(a, b, c, d) 就是 dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)

我们来看看 discreteUpdatesImpl 具体执行的是什么?

  1. 生成新函数func = fn.bind(null, a, b, c, d), 即如下
    • func = dispatchEvent.bind(null, domEventName, eventSystemFlags, container, nativeEvent)
  2. discreteUpdatesImpl 执行和 用户阻塞事件监听器 (第二类) 调用同一个函数
/**
* discreteUpdatesImpl: 离散更新函数
*/
discreteUpdatesImpl = function discreteUpdates(fn, a, b, c, d) {
    // ...
    try {
        /**
        * 这里和第二类监听处理就一模一样
        * runWithPriority => Scheduler_runWithPriority => Scheduler.unstable_runWithPriority
        * UserBlockingSchedulerPriority: 任务优先级 98
        * fn.bind(null, a, b, c, d) => func: func() 执行不多做赘述
        */
        return runWithPriority(
            UserBlockingSchedulerPriority, // 98
            fn.bind(null, a, b, c, d)
        );
    } finally {
        // ...
     }
}

看过**离散事件监听器** 和 用户阻塞事件监听器, 我们可以了解这两种事件处理 最后 执行的是 第三类连续事件或其他事件监听器 dispatchEvent

一文帮你熟悉 React17 事件机制 (二)

连续事件或其他事件监听器: 第三类 dispatchEvent (重点)

dispatchEvent 代码: 简化版本
// 尝试调度一个事件. 如果被阻止, 则返回 SuspenseInstance 或 Container
function attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
    // 获取原生dom节点: 获取监听器中传进来的 Event 对象, 并获取 nativeEvent.target 内的 DOM 节点
    var nativeEventTarget = getEventTarget(nativeEvent);
    // 获取原生dom节点对应的fiber
    var targetInst = getClosestInstanceFromNode(nativeEventTarget);
    // 下面 判断fiber节点的类型以及是否已渲染来决定是否要派发事件
    // 得到的 Fiber 实例可能有一些问题, 不是想要的, 这里做兼容, 可忽略
    // ...
    dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer);
    return null;
}
/**
*
* 当原生事件被触发后, 进入dispatchEvent
* 调用 dispatchEventsForPlugins: 这个函数触发了 事件收集、事件执行
* @param {*} domEventName : DOM 事件名称 click
* @param {*} eventSystemFlags : 事件系统标记
* @param {*} targetContainer : root DOM 元素
* @param {*} nativeEvent : 原生事件 (from: addEventListener)
* @returns 
*/
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
    // ...
    // 尝试调度事件
    // - 如果被阻止, 则返回 SuspenseInstance 或 Container, 拿到原生DOM节点或该节点对应的fiber
    // = 如果调度, 直接运行 dispatchEventForPluginEventSystem, 返回 null
    var blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
    if (blockedOn === null) {
      // ...
      return;
    }
    // ...
    // 通过插件系统,触发事件
    dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
}
  • 上面经过处理, 就是执行 dispatchEventForPluginEventSystem函数
  • attemptToDispatchEvent 这里我们还能了解到, React fiber 和 dom 实例如何建立关联

一文帮你熟悉 React17 事件机制 (二)

dispatchEventForPluginEventSystem
  • 这个函数做一件事: 执行 dispatchEventsForPlugins
/**
* 当页面上触发了特定的事件时, 如点击事件click, 就会触发绑定在根元素上的事件回调函数
* 即之前绑定了参数的dispatchEvent, 在内部最终会调用dispatchEventsForPlugins
* @param {*} domEventName DOM 事件名称 click
* @param {*} eventSystemFlags : 事件系统标记
* @param {*} nativeEvent : 原生事件
* @param {*} targetInst : Fiber, 点击元素的Fiber
* @param {*} targetContainer: 容器节点DOM
* @returns 
*/
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
    var ancestorInst = targetInst;
    // ...
    
    /**
     * fn = function () {
           return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
       }
     * 这个是偏函数 就是函数调用, 执行 batchedEventUpdates(fn, a, b) => fn(a) => dispatchEventsForPlugins(...)
     * 最终执行 dispatchEventsForPlugins
     */
    batchedEventUpdates(function () {
      return dispatchEventsForPlugins(
          domEventName, eventSystemFlags, nativeEvent, ancestorInst
      );
    });
}
var batchedEventUpdatesImpl = batchedUpdatesImpl = function(fn, bookkeeping) {
  return fn(bookkeeping);
}
function batchedEventUpdates(fn, a, b) {
    // 是否在事件执行批次中
    if (isBatchingEventUpdates) {
      return fn(a, b);
    }
    isBatchingEventUpdates = true;
    try {
      // 执行函数
      // batchedEventUpdatesImpl => batchedUpdatesImpl =>(fn, a) => fn(a)
      return batchedEventUpdatesImpl(fn, a, b);
    } finally {
      isBatchingEventUpdates = false;
      finishEventHandler();
    }
}
dispatchEventsForPlugins
  • React 事件函数收集 + 函数处理
  • 收集是根据事件的类型分别处理的, extractEvents的入参分别给各个事件处理插件的extractEvents进行分别处理
  • processDispatchQueue处理收集函数
    • 捕获阶段 事件队列后序遍历执行
    • 冒泡阶段 事件队列顺序遍历执行
    • 原因: 事件收集是 从目标元素 冒泡 到 root 收集的 事件队列
/**
* 冒泡 收集函数  +  批处理函数
* @param {*} domEventName : dispatchEvent中绑定的事件名 click
* @param {*} eventSystemFlags : dispatchEvent绑定的事件标记
* @param {*} nativeEvent : 事件触发时回调传入的原生事件对象
* @param {*} targetInst : 事件触发目标元素对应的fiber
* @param {*} targetContainer :  (一般undefined, 暂时没找到哪里传)
*/
function dispatchEventsForPlugins(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer
) {
    // 获取了一遍 event.target dom
    var nativeEventTarget = getEventTarget(nativeEvent);
    // 事件队列, 收集到的事件都会存储到这
    var dispatchQueue = [];
    // 下面两个是重点 收集 & 处理
    // 收集事件: 进行事件合成
    // 这里源码执行的是一个处理集合: 
    /*
    这里源码执行的是一个处理集合: 
    {
        // extractEvents$4
        SimpleEventPlugin.extractEvents(...); // 大部分事件收集
        
        // mouseover, mouseout, pointerover, pointerout 处理
        // extractEvents$2
        EnterLeaveEventPlugin.extractEvents(...); 
        
        // extractEvents$1
        ChangeEventPlugin.extractEvents(...); // input, textarea, select 相关的事件 处理
        
        // focusin, focusout, mousedown, contextmenu, mouseup,
        // dragend, selectionchange, keydown, keyup 事件处理
        // extractEvents$3
        SelectEventPlugin.extractEvents(...); 
        
        // compositionEvent: compositionend, compositionstart, compositionupdate
        // beforeInputEvent: beforeinput  处理
        // extractEvents
        BeforeInputEventPlugin.extractEvents(...); 
    }
    直接看这个 impleEventPlugin.extractEvents (extractEvents$4) 就可以
    */
    extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
    // 执行事件: 根据事件阶段(冒泡或捕获)来决定是正序还是倒序遍历合成事件中的listeners
    /**
     * dispatchQueue 存储: 单个类型事件会存在一个单元
     * [
     *    {
     *      event: click 事件对象,
     *      listeners: [{handler}, ...], 所有冒泡 (捕获) 上元素事件集合
     *    },
     *    {
     *      event: change 事件对象,
     *      listeners: [{handler}, ...], 所有冒泡 (捕获)上元素change事件集合
     *    },
     *    ....
     * ]
     */
    processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents 的内容其实很简单, 按需调用几个 EventPluginextractEvents, 这几个 extractEvents 的目的是一样的, 只不过针对不同的事件可能会生成不同的事件, 我们以最核心的也是最关键的 SimpleEventPlugin.extractEvents 来讲解

SimpleEventPlugin.extractEvents 事件函数收集

click 为例子, click 合成事件的构造函数 SyntheticEvent

  • accumulateSinglePhaseListeners 获取当前事件的 所有react 事件函数, 返回 函数队列:
  • listeners = [{instance, listener, currentTarget}, ...]
  • event = new SyntheticEventCtor (SyntheticEvent) 生成合成事件的 Event 对象
    • 抹平浏览器之间差异
    • 关键方法的包装
  • 最终返回需要执行事件队列 dispatchQueue: [{event, listeners}, ...]
/**
* 收集
* 先执行捕获事件, 在执行冒泡事件
* onClick: 冒泡事件
* onClickCapture: 捕获事件
* 都是根据触发节点向上匹配
* - 先匹配某个类型事件(click)所有捕获事件集合, 派发执行
* - 再匹配某个类型事件(click)所有冒泡事件集合, 派发执行
* @param {*} dispatchQueue : 事件队列
* @param {*} domEventName : 事件名称
* @param {*} targetInst : 目标元素 fiber
* @param {*} nativeEvent : 原生事件对象
* @param {*} nativeEventTarget 
* @param {*} eventSystemFlags : 系统标识
* @param {*} targetContainer : undefined
* @returns 
*/
function extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
    // 根据原生事件名称获取合成事件名称
    var reactName = topLevelEventsToReactNames.get(domEventName);
    if (reactName === undefined) {
      return;
    }
    // 默认合成函数的构造函数
    var SyntheticEventCtor = SyntheticEvent;
    var reactEventType = domEventName;
    // 按照原生事件名称来获取对应的合成事件构造函数
    switch (domEventName) {
        /*
        SyntheticEventCtor = SyntheticKeyboardEvent;
        SyntheticEventCtor = SyntheticFocusEvent;
        SyntheticEventCtor = SyntheticFocusEvent;
        SyntheticEventCtor = SyntheticFocusEvent;
        SyntheticEventCtor = SyntheticMouseEvent;
        SyntheticEventCtor = SyntheticDragEvent;
        SyntheticEventCtor = SyntheticTouchEvent;
        SyntheticEventCtor = SyntheticAnimationEvent;
        SyntheticEventCtor = SyntheticTransitionEvent;
        SyntheticEventCtor = SyntheticUIEvent;
        SyntheticEventCtor = SyntheticWheelEvent;
        SyntheticEventCtor = SyntheticClipboardEvent;
        SyntheticEventCtor = SyntheticPointerEvent;
        */
    }
    // 是否是捕获阶段
    var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
    // scroll 事件不冒泡
    var accumulateTargetOnly =!inCapturePhase && domEventName === 'scroll';
    // 获取当前阶段的所有事件
    var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
    if (_listeners.length > 0) {
        // 生成合成事件的 Event 对象: click
        // 不同类型处理不一样
        var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
        // 入队
        dispatchQueue.push({
          event: _event,
          listeners: _listeners
        });
    }
}
  • accumulateSinglePhaseListeners: 累计click事件当前阶段 (冒泡 或者 捕获) 所有事件
    • 捕获阶段收集 onClickCapture 函数
    • 冒泡阶段收集 onClick 函数
    • 无论冒泡还是捕获阶段, React 事件收集从事件触发目标元素 fiber 冒泡 到root fiber, 收集fiber props 里面的对应事件函数 (React 虚拟冒泡)
    • 返回 事件函数集合: [{instance, listener, lastHostComponent}, ....]
      • Instance: 目标元素fiber, 事件处理中不会变, event.target 对应元素Fiber
      • Listener: React 事件函数, 例如 onClick (fn)
      • currentTarget: 绑定React事件 的dom 元素
/**
* 累计click事件当前阶段 (冒泡 或者 捕获) 所有事件
*    - 捕获阶段收集 onClickCapture 函数
    - 冒泡阶段收集 onClick 函数
    - 无论冒泡还是捕获阶段, React 事件收集从事件触发目标元素 fiber 冒泡 到root fiber, 
      收集fiber props 里面的对应事件函数  (React 虚拟冒泡)
* @param {*} targetFiber : 目标fiber
* @param {*} reactName : React事件名称 onClick
* @param {*} nativeEventType : 原生事件名称(类型即名称) click
* @param {*} inCapturePhase : 是否捕获阶段 调用的委托事件触发器
* @param {*} accumulateTargetOnly 
* @returns 
*/
function accumulateSinglePhaseListeners(
    targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly
) {
    // 捕获函数名称 onClickCapture
    var captureName = reactName !== null ? reactName + 'Capture' : null;
    // 最终合成事件名称
    var reactEventName = inCapturePhase ? captureName : reactName;
    // 事件函数 队列
    var listeners = [];
    // 目标元素 fiber
    var instance = targetFiber;
    // 这个收集 当前React事件绑定的元素
    // 还记得之前说的 事件绑定元素 和 目标元素么?
    var lastHostComponent = null;
    // 从目标元素开始一直到root,累加所有的fiber对象和事件监听
    while (instance !== null) {
      var _instance2 = instance,
          // fiber 对应的 dom 元素, fiber.stateNode
          stateNode = _instance2.stateNode,
          tag = _instance2.tag; // Handle listeners that are on HostComponents (i.e. <div>)
      // 有效节点则获取其事件
      if (tag === HostComponent && stateNode !== null) {
        // 这里设置 当前 dom 元素
        lastHostComponent = stateNode;
        if (reactEventName !== null) {
          // 获取存储在 Fiber 节点上 Dom Props 里的对应事件
          // instance: fiber.stateNode[internalPropsKey][reactEventName]
          var listener = getListener(instance, reactEventName);
          if (listener != null) {
            // 入对
            // 将返回 {instance, listener, currentTarget: lastHostComponent} 对象 入队
            listeners.push(
                /**
                 * 简单返回一个 当前事件 {instance, listener, currentTarget: lastHostComponent} 对象
                 * instance: fiber
                 * listener: 事件处理函数
                 * currentTarget: dom 绑定函数的dom, 事件需要指向currentTarget
                 */
                createDispatchListener(
                    instance, listener, lastHostComponent
                )
            );
          }
        }
      }
      
      // scroll 不会冒泡, 获取一次就结束了
      if (accumulateTargetOnly) {
        break;
      }
      // 其父级 Fiber 节点, 向上递归
      instance = instance.return;
    }
    // 该事件名(如click) 对应收集的监听器(执行函数)
    return listeners;
  }

一文帮你熟悉 React17 事件机制 (二)

  • new SyntheticEventCtor: SyntheticEvent = createSyntheticEvent(EventInterface)
    • 根据事件类型对原生事件的属性做浏览器的抹平
    • 关键方法的包装
// 辅助函数,永远返回true
function functionThatReturnsTrue() {  return true;}
// 辅助函数,永远返回false
function functionThatReturnsFalse() {  return false;}
var EventInterface = {
    eventPhase: 0,
    bubbles: 0,
    cancelable: 0,
    timeStamp: function (event) {
      return event.timeStamp || Date.now();
    },
    defaultPrevented: 0,
    isTrusted: 0
};
// 这个其实就是 SyntheticBaseEvent 函数
// 使用通过给工厂函数传入事件接口获取事件合成事件构造函数
var SyntheticEvent = createSyntheticEvent(EventInterface);
function createSyntheticEvent(Interface) {
    // 合成事件构造函数
    function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
      // react事件名
      this._reactName = reactName;
      // 当前执行事件回调时的 fiber (传入的是null)
      this._targetInst = targetInst;
      // 真实事件名
      this.type = reactEventType;
      // 原生事件对象
      this.nativeEvent = nativeEvent;
      // 原生触发事件的DOM target
      this.target = nativeEventTarget;
      // 当前执行回调的DOM
      this.currentTarget = null;
      /**
       * 抹平字段在浏览器间的差异
       * 内部要么函数 & 要么是属性 xxx: 0
       * {
       *    xxx: 0,
       *    // or
       *    xxx: function () {}
       * }
       */
      for (var _propName in Interface) {
        if (!Interface.hasOwnProperty(_propName)) {
          // 该接口没有这个字段, 不拷贝
          continue;
        }
        // 拿到事件接口对应的值
        var normalize = Interface[_propName];
        // 接口对应字段函数, 进入if分支, 执行函数拿到值
        if (normalize) {
          // 抹平了浏览器差异后的值
          this[_propName] = normalize(nativeEvent);
        } else {
          // 接口对应值是0, 则直接取原生事件对应字段值
          this[_propName] = nativeEvent[_propName];
        }
      }
      // 抹平defaultPrevented的浏览器差异,即抹平e.defaultPrevented和e.returnValue的表现
      var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
      if (defaultPrevented) {
        // 在处理事件时已经被阻止默认操作了, 调用isDefaultPrevented一直返回true
        this.isDefaultPrevented = functionThatReturnsTrue;
      } else {
        // 在处理事件时没有被阻止过默认操作, 则先用返回false的函数
        this.isDefaultPrevented = functionThatReturnsFalse;
      }
      // 默认执行时, 还没有被阻止继续传播, 调用isPropagationStopped返回false
      this.isPropagationStopped = functionThatReturnsFalse;
      return this;
    }
    _assign(SyntheticBaseEvent.prototype, {
      // 阻止默认事件
      preventDefault: function () {
        // 调用后设置 defaultPrevented
        this.defaultPrevented = true;
        var event = this.nativeEvent;
        if (!event) {
          return;
        }
        // e.preventDefault() 和 e.returnValue=false 的浏览器差异, 并在原生事件上执行
        if (event.preventDefault) {
          event.preventDefault(); // $FlowFixMe - flow is not aware of `unknown` in IE
        } else if (typeof event.returnValue !== 'unknown') {
          event.returnValue = false;
        }
        // 后续回调判断时都会返回true
        this.isDefaultPrevented = functionThatReturnsTrue;
      },
      // 阻止捕获和冒泡阶段中当前事件的进一步传播
      stopPropagation: function () {
        var event = this.nativeEvent;
        if (!event) {
          return;
        }
        // e.stopPropagation() 和 e.calcelBubble = true的差异, 并在原生事件上执行
        if (event.stopPropagation) {
          event.stopPropagation(); // $FlowFixMe - flow is not aware of `unknown` in IE
        } else if (typeof event.cancelBubble !== 'unknown') {
          event.cancelBubble = true;
        }
        // 然后后续判断时都会返回true, 停止传播
        this.isPropagationStopped = functionThatReturnsTrue;
      },
      // 合成事件不使用对象池了, 这个事件是空的没有意义
      persist: function () {},
      isPersistent: functionThatReturnsTrue
    });
    return SyntheticBaseEvent;
}

事件收集在这里就执行完了, 我们用下面图表示下流程

一文帮你熟悉 React17 事件机制 (二)

processDispatchQueue: 处理收集函数
  • processDispatchQueue: 遍历事件队列
    • 队列每个单元对应的是一类事件 所有事件, 因此需要遍历执行
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
    // 通过 eventSystemFlags 判断当前事件阶段
    // 是否捕获阶段
    var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
    // 遍历合成事件
    for (var i = 0; i < dispatchQueue.length; i++) {
      // 取出其合成 Event 对象及事件集合
      var _dispatchQueue$i = dispatchQueue[i],
          event = _dispatchQueue$i.event,
          listeners = _dispatchQueue$i.listeners;
      // 这个函数就负责事件的调用
      // 如果是捕获阶段的事件则倒序调用, 反之为正序调用, 调用时会传入合成 Event 对象
      processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
    }
    
    // 错误抛出
    rethrowCaughtError();
  }
  • processDispatchQueueItemsInOrder: 根据不同阶段处理事件的调用
    • 捕获阶段: 后序遍历处理执行
    • 冒泡阶段: 顺序遍历处理执行
    • 如果中间有函数里面调用 e.stopPropagation() 阻止事件执行
      • onClick = e => e.stopPropagation()
      • event.isPropagationStopped() => functionThatReturnsTrue = () => true 返回true
      • 后面函数执行就不会进行了
// 负责事件的调用
function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {
    var previousInstance;
    if (inCapturePhase) {
      for (var i = dispatchListeners.length - 1; i >= 0; i--) {
        var _dispatchListeners$i = dispatchListeners[i],
            instance = _dispatchListeners$i.instance,
            currentTarget = _dispatchListeners$i.currentTarget,
            listener = _dispatchListeners$i.listener;
        // onClick = e => e.stopPropagation()后
        // event 的 isPropagationStopped() => functionThatReturnsTrue = () => true
        // 那么就不会在调用后面的listener了,下面的冒泡同理
        if (instance !== previousInstance && event.isPropagationStopped()) {
          return;
        }
        executeDispatch(event, listener, currentTarget);
        previousInstance = instance;
      }
    } else {
       // 和上面一样, 不过是 for (var _i = 0; _i < dispatchListeners.length; _i++)
       // 顺序执行
    }
}
  • executeDispatch: 函数执行
    • 由于合成事件对象时候 currentTarget 是null, 调用函数时候保证能感知绑定元素, 需要 给event.currentTarget 赋值
    • 执行完成后需要清空 event.currentTarget
function executeDispatch(event, listener, currentTarget) {
    var type = event.type || 'unknown-event';
    /**
     * 这一步是必须的, 保证事件对象的完整性
     * currentTarget: 正在执行的监听函数所绑定的那个节点
     */
    // 设置合成事件执行到当前DOM实例时的指向
    event.currentTarget = currentTarget;
    // (name, func, context, ....arg)
    // var funcArgs = Array.prototype.slice.call(arguments, 3);
    // func.apply(context, funcArgs); => listener.apply(undefined, event)
    // 运行函数 且 错误收集
    invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
    // 不在事件的回调中时拿不到currentTarget
    event.currentTarget = null;
}
下次事件重新触发: 重新走捕获冒泡流程, React 事件机制重新收集 + 处理
  • React 事件每次执行为什么要重新收集, 没有缓存?
    • 每一次事件触发后, 原有的树结构发生改变, 历史收集的 事件有可能已经不存在
    • 不同触发期间 事件函数有可能根据状态的变化 发生改变, 此时旧事件就无效了
    • 由于闭包, React 事件函数本身是一个闭包, 当运行缓存函数时候, 存储的状态不是最新的, 此时事件处理状态不是最新的

自此, 事件机制就结束了, 总结下来:

  • React 合成事件系统其实并不算特别复杂, 其核心思想就是用事件代理统一接收事件的触发, 然后由 React 自身来调度真实事件的调用, 而 React 如何知道应该从哪开始收集事件的核心其实是存储在真实 DOM 上的 Fiber 节点, 从真实穿梭到虚拟。
  • React 17 利用原生 捕获和冒泡 事件的支持, 对齐了浏览器原生标准, 同时 onScroll 事件不再进行事件冒泡, onFocus 和 onBlur 使用原生 focusin & focusout 合成。
  • React 中事件触发的本质是对 dispatchEvent 函数的调用, 模拟原生的事件的捕获和冒泡, 监听委托事件执行, 根据目标元素Fiber 树, 冒泡收集事件, 顺序执行。

文档

  1. UI Events | W3C Working Draft, 04 August 2016
  2. React event object
  3. Event - Web API 接口参考 | MDN
  4. Introduction to events
  5. React v17.0 RC 发版声明
  6. 深入理解js事件机制
  7. 深入React合成事件机制原理