【React源码笔记17】- 事件系统

React | 2020-08-25 22:33:27 739次 1次

react 中实现了一套事件系统,将事件绑定到 document 上,主要为了跨平台以及事件优先级的调度,还可以兼容不同浏览器等优点。


一、总体设计

/**
 *
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|PluginRegistry|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 */


【ReactEventListener 】

概念: 负责给元素绑定事件


【ReactEventEmitter】

概念: 暴露接口给 React 组件层用于添加事件订阅(对外暴露了 listenTo 等方法)


【EventPluginHub】

概念:负责管理和注册各种插件


【plugin 插件】

SimpleEventPlugin:blur、focus、click、submit、touchMove、mouseMove、scroll、drag、load
        
EnterLeaveEventPlugin:mouseEnter/mouseLeave 和 pointerEnter/pointerLeave 

ChangeEventPlugin: onChange(比较复杂)


SelectEventPlugin:select选择

BeforeInputEventPlugin

...

执行过程:事件绑定 ---> 合成事件列表获取 --> 事件触发。


二、事件分类和优先级

DiscreteEvent(离散事件)click,blur,focus,submit,tuchStart 等,优先级是 0

UserBlockingEvent(用户阻塞事件)touchMove,mouseMove,scroll,drag,dragOver 等,这些事件会阻塞用户的交互,优先级是 1。

ContinuousEvent(连续事件)load,error,loadStart,abort,animationend 等,优先级是 2,这个优先级最高,不会被打断。


优先级:

Immediate - 这个优先级的任务会同步执行, 或者说要马上执行且不能中断-----------对应【ContinuousEvent】

UserBlocking(250ms timeout) 这些任务一般是用户交互的结果, 需要即时得到反馈 .----对应【UserBlockingEvent、DiscreteEvent】

Normal (5s timeout) 应对哪些不需要立即感受到的任务,例如网络请求

Low (10s timeout) 这些任务可以放后,但是最终应该得到执行. 例如分析通知

Idle (no timeout) 一些没有必要做的任务 (比如隐藏的内容).


三、事件绑定

    事件绑定大致流程:                
                
                complate
                   |
                finalizeInitialChildren
                   |
                setInitialProperties
                   |
                setInitialDOMProperties 根据不同的属性类别进行操作,如果是事件类型才会继续往下走
                   |
                ensureListeningTo
                   |
                legacyListenToEvent
                   |
                legacyListenToTopLevelEvent
                   |
                trapBubbledEvent | trapCapturedEvent 捕获或者冒泡
                   |
                trapEventForPluginEventSystem  根据优先级调度
                   |
                addEventCaptureListener 监听


属性遍历

setInitialDOMProperties 方法会遍历所有的属性,执行对应的属性处理:

function setInitialDOMProperties(...): void {
  // 一个大循环遍历所有属性
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      // style 属性设置
      setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      ...setInnerHTML(domElement, nextHtml);
    } else if (propKey === CHILDREN) {
      ...
      // string | number 才会设置setTextContent
    } else if (registrationNameModules.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        // 绑定了事件,进行下一步操作
        ensureListeningTo(rootContainerElement, propKey);
      }
    } else if (nextProp != null) {
      // 其他就是设置属性值
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

这里有个 registrationNameModules,这个就是插件的一个注册过程,这个过程是在页面加载时候就会执行,里面记录一些固定的事件写法,所以可以判断是否为事件类型,大致过程在下一小节。


事件分发(冒泡 | 捕获)

在 legacyListenToTopLevelEvent 中会对一些不冒泡的事件直接进行 trapCapturedEvent,不会冒泡的事件:

- scroll

- blur & focus

- Media 事件

- mouseleave &  mouseenter

其他的事件进行 trapBubbledEvent

但是代码中可以看到没有对 Form 事件和 Media 事件进行委托,因为这些事件委托后会触发两次回调函数:

function legacyListenToTopLevelEvent(...): void {
  if (!listenerMap.has(topLevelType)) {
    switch (topLevelType) {
      ...
      case TOP_INVALID:
      case TOP_SUBMIT:
      case TOP_RESET:
        break;
      default:
        // 不是 media 事件才会冒泡
        if (!isMediaEvent) {
          trapBubbledEvent(topLevelType, mountAt);
        }
        break;
    }
  }
}


优先级事件创建

这一步主要是创建出不同优先级的事件回调,监听后等待被执行:

function trapEventForPluginEventSystem(
  container: Document | Element | Node,
  topLevelType: DOMTopLevelEventType,
  capture: boolean,
): void {
  let listener;
  // topLevelType  就是特定的事件名称,反查一下
  switch (getEventPriorityForPluginSystem(topLevelType)) {
    case DiscreteEvent:
      // 一个开关控制,等待上一个执行完毕再执行当前事件
      listener = dispatchDiscreteEvent.bind(...);
      break;
    case UserBlockingEvent:
      // 会同步执行
      listener = dispatchUserBlockingUpdate.bind(...);
      break;
    case ContinuousEvent:
    default:
        // 立即执行
      listener = dispatchEvent.bind(...);
      break;
  }
   
  // 创建监听到root,创建出的listener作为回调
  if (capture) {
    addEventCaptureListener(container, rawEventName, listener);
  } else {
    addEventBubbleListener(container, rawEventName, listener);
  }
}

最终这里是要创建出一个 listener(dispatchEvent) 回调,其中 DiscreteEvent 需要单独包装一下,按照顺序异步执行,其他两个同步执行,里面具体做了什么等下面调用时候再看。

事件注册的过程结束。


四、插件注册

插件注册的过程是独立的,页面加载完就会去执行,大致流程:

        injectEventPluginsByName        
                  |
        recomputePluginOrdering
                  |
        publishEventForPlugin
                  |
        publishRegistrationName --  给 registrationNameModules 赋值 {onClick: {}, ...}

参数主要是由入口函数传递进入的:

injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
});


SimpleEventPlugin 数据结构

传入不同的事件插件,其中 SimpleEventPlugin 的数据结构如下:

{
    eventTypes: {
        phasedRegistrationNames: {
            bubbled: 'onClick',  //冒泡
            captured: 'onClick' + 'Capture',  //捕获
        },
       dependencies: ['onClick'],
       eventPriority: 1,  //优先级
    },
    extractEvents: () => {}
}

初始的执行会创建出一套这样的针对不同平台的事件对象(具体方法由个平台传入,本次看到的是 dom 层面定义的一些东西,具体的创建代码简要贴一下):

const discreteEventPairsForSimpleEventPlugin = [
    DOMTopLevelEventTypes.TOP_CLICK, 'click',
    DOMTopLevelEventTypes.TOP_CLOSE, 'close',
    ...
]

function processSimpleEventPluginPairsByPriority(...): void {
  for (let i = 0; i < eventTypes.length; i += 2) {
    // 指定的结构
    const config = {
      phasedRegistrationNames: {
        bubbled: onEvent,
        captured: onEvent + 'Capture',
      },
      dependencies: [topEvent],
      eventPriority: priority,
    };
    // 这个值给到 eventTypes 上,并最终创建出 SimpleEventPlugin 的事件列表
    simpleEventPluginEventTypes[event] = config;
  }
}

// 初次执行这个的时候就已经确定好了优先级,因为事件和优先级关系就是react指定的
processSimpleEventPluginPairsByPriority(discreteEventPairsForSimpleEventPlugin, DiscreteEvent)


extractEvents 合成事件

extractEvents 方法会等到下个阶段事件执行(extractPluginEvents)时候再被触发,大致逻辑:

...
const event = EventConstructor.getPooled(...);
// 模拟捕获和传播
accumulateTwoPhaseDispatches(event);
// 返回这个事件,后面使用
return event;
...

EventConstructor 来自 SyntheticEvent 基类,这个类可以被扩展,其实就是 extend 方法中实现一个继承机制,这样一些其他事件可以使用基类一些公共方法,比如上面的 getPooled 方法,是从对象池中获取数据,避免对象的重复创建,减少 GC 开销。

模拟捕获和冒泡事件过程,最终挂载到 event._dispatchListeners 上,它存储了所有需要触发的监听函数,比如父子都绑定了事件,这里需要全部记录,从而可以模拟冒泡和捕获的执行:

export function traverseTwoPhase(inst, fn, arg) {
  const path = [];
  while (inst) {
    path.push(inst);
    // 往上寻找
    inst = getParent(inst);
  }
  let i;
  // 这个过程创建的对象最终挂载到 event._dispatchListeners 上
  for (i = path.length; i-- > 0; ) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

举例:

<div onClick = { A }>
    <div onClickCapture = { B }>
        <div onClick = { C }>
        </div>
    </div>
</div>  

最终 event._dispatchListeners 顺序为 [ B, C, A ]  因为中间这个事件定义为 【捕获阶段触发】

----------------------------------------------------------------------
<div onClick = { A }>
    <div onClick = { B }>
        <div onClick = { C }>
        </div>
    </div>
</div>  

最终 event._dispatchListeners 顺序为 [ C, B, A ]  默认为 【冒泡阶段触发事件】


五、事件执行

大致流程:
                        dispatchEvent
                            |
                        attemptToDispatchEvent
                            |
                        dispatchEventForLegacyPluginEventSystem 
                            |
                        batchedEventUpdates 批量更新合成事件,同一个dom触发的多个合成事件
                            |
                        batchedEventUpdates$1
                            |
                        handleTopLevel
                            |
                        runExtractedPluginEventsInBatch
                            |
                        extractPluginEvents 执行上面插件方法,创建出一个合成事件列表
                            
                        【SimpleEventPlugin->extractEvents】触发插件
                            |
                        accumulateInto
                            |
                        上面会取到一个合成事件列表,批量执行
                        runEventsInBatch
                            |
                       executeDispatchesInOrder
                                 |
                           executeDispatch
                                 | 
                        finishEventHandler
                            |
                       如果此时发生数据改变则开始调度
                        runWithPriority$1
                        Scheduler_runWithPriority
                        flushSyncCallbackQueue

事件执行的时候做的事情很多,比如事件监听回调中做了什么,插件中预留的那个方法进行事件列表处理,最终怎么执行,又如何调度更新等问题,逐步来展开分析。


dispatchEvent回调

dispatchEvent 中调用了 attemptToDispatchEvent

// 这里接收四个参数,在dispatchDiscreteEvent.bind(...) 中传入了四个参数
// 第一个为绑定对象 null,剩余三个 topLevelType PLUGIN_EVENT_SYSTEM  container(document)
// 事件监听触发时传入第四个参数,就是触发的目标节点 nativeEvent
export function attemptToDispatchEvent(
  topLevelType: DOMTopLevelEventType,
  eventSystemFlags: EventSystemFlags,
  container: Document | Element | Node,
  nativeEvent: AnyNativeEvent,
){

  // 从目标事件身上取到 target真实dom
  const nativeEventTarget = getEventTarget(nativeEvent);
  
  // 根据真实dom获取fiber对象  internalInstanceKey:一个字符串+随机数
  // createInstance时候创建关系  precacheFiberNode方法中
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  ...

  ...
  dispatchEventForLegacyPluginEventSystem(
      topLevelType,
      eventSystemFlags,
      nativeEvent,
      targetInst,
  );
  return null;
}

这个方法中第四个参数是执行事件监听回调时传入,获取对应的 target 之后,再根据真实 dom 取出对应的 fiber 节点对象,fiber 和真实 dom 的关系创建在  createInstance 时候创建

继续进入 dispatchEventForLegacyPluginEventSystem

export function dispatchEventForLegacyPluginEventSystem(...): void {
  const bookKeeping = getTopLevelCallbackBookKeeping(...);

  try {
    // 批量更新
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

这个过程中代码很少,但是进入之后的各种回调相当复杂,获取到 bookKeeping 事件列表之后进行批量执行 _dispatchListeners


批量执行

可以看到获取 event._dispatchListeners 之后,需要对当前触发点整条链路相关的节点注册的事件进行批量执行,注意和批量更新的区别。

在第四部分那里得知,执行完 合成事件 创建之后,会返回这个 event 对象,从 runEventsInBatch 方法开始:

function runEventsInBatch(events) {
  if (events !== null) {
    // 这个方法和插件注册执行那里一致,取到一个合成事件列表
    eventQueue = accumulateInto(eventQueue, events);
  } 
  var processingEventQueue = eventQueue;
  ...
  
  // 这个方法就是一个遍历,执行传入的回调
  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
  ...
}

// executeDispatchesAndReleaseTopLevel 方法执行
var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) {
      if (event) {
        executeDispatchesInOrder(event);
        
        // 执行完事件回调后,如果事件没有调用 persist 方法,那就释放对象,
        // 但是释放后的对象上属性值都为 null,此时如果数据池不满则放回数据池
        if (!event.isPersistent()) {
          event.constructor.release(event);
        }
      }
};

executeDispatchesInOrder 方法中会判断是否阻止冒泡,并执行掉事件:

function executeDispatchesInOrder(event) {
      var dispatchListeners = event._dispatchListeners;
      var dispatchInstances = event._dispatchInstances;
      
      if (Array.isArray(dispatchListeners)) {
        for (var i = 0; i < dispatchListeners.length; i++) {
          // 阻止冒泡
          // 开发者主动调用e.stopPropagation(),react将isPropagationStopped设置为返回 true 的一个函数
          if (event.isPropagationStopped()) {
            break;
          } 
          executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
        }
      } else if (dispatchListeners) {
        // 单个事件
        executeDispatch(event, dispatchListeners, dispatchInstances);
      }
      // 重置
      event._dispatchListeners = null;
      event._dispatchInstances = null;
    }

最终在 executeDispatch 方法中执行 dispatchListeners 回调(也就是绑定的合成事件)


六、总结

react 事件系统的原理进行了一个大致的梳理,其中有一些细节的实现没有详细展开的分析,比如事件池的使用、其他类型事件,等有时间再逐步补充,尤其是 onChange 事件的设计很复杂。

1人赞

分享到: