React 事件机制-v16.13.1

前端 @ 北京字节跳动科技有限公司

概览

所谓事件机制,无非就是注册事件与分发事件两个步骤,程序员通过代码来注册事件到react对应的节点上,用户与浏览器发生交互,触发浏览器的原生事件,原生事件被react捕获,从原生事件中拿到原生节点,然后根据原生节点拿到react设计的fiber节点,然后从fiber节点中取出相应的回调来执行它,简单流程就是如此,所以我们这次讨论react事件机制,就是如何注册,以及如何分发

取出事件

react初始化过程,简单来讲就是我们的jsx被babel转成一串json,我们将这串json叫做react元素的树,react再将这串json转成fiber树(fiber树主要用来做diff),生成fiber树的过程其实就是,我们对react元素的树进行一个深度优先的遍历,遍历的过程中,对于每个节点,我们所绑定的事件其实是作为一个属性绑定在上面的,遍历完成后,我们需要生成一颗真实DOM的树,生成的时候,每个真实DOM节点都会和fiber节点关联起来(通过一个key值internalInstanceKey

注册事件

在我们生成真实DOM树的时候,需要将fiber树的一些属性(比如样式)映射到真实DOM上,当发现这个节点绑定了事件的时候,我们直接将这个事件类型绑定到根节点或者document上

1  function addEventBubbleListener(target, eventType, listener) {
2    console.log(target, eventType, listener.name);
3    target.addEventListener(eventType, listener, false);
4    return listener;
5  }
复制代码

react监听的是根节点的事件,所以listener是一个能够处理所有节点的响应事件的函数,最终浏览器中注册的事件应该是下面这样,每个事件类型,都在根节点上注册了一个相应的listener进行事件的分发。

分发事件

分发事件,简单来讲就是事件被触发后,从内存取出对应的回调来执行,当用户与浏览器产生交互时,以下事情会先后执行。

拿到原生event

触发原生事件,拿到原生事件的event

完整代码地址:addEventCaptureListener

1  document.addEventListener(type, dispatchEvent)
2  function dispatchEvent(event) {
3      console.log(event) // 从这里拿到event
4  }
复制代码

拿到fiber节点

根据原生事件的event可以拿到原生DOM节点,继而拿到其fiber节点。(在生成真实dom树的时候,dom节点就已经通internalInstanceKey和fiber节点关联起来了)

完整代码地址:attemptToDispatchEvent

1  const nativeEventTarget = getEventTarget(nativeEvent);
2  const targetInst = getClosestInstanceFromNode(nativeEventTarget);
3
4  function getClosestInstanceFromNode(targetNode: Node) {
5    let targetInst = (targetNode: any)[internalInstanceKey];
6    if (targetInst) {
7      // Don't return HostRoot or SuspenseComponent here
8      return targetInst;
9    }
10  }
复制代码

合成react事件

根据原生事件开始合成react事件,在此之前,先大概介绍以下react合成事件这个概念。

核心概念

浏览器的事件,大多数直接派生自Event,另外有7类属于UI事件,派生自UIEventUIEvent派生自Event。react内部也是如此。

细节就不再过多赘述了,主要做的事是

  1. 对原生事件的封装,基本上是把原生事件的一些属性在代码内部自己定义了一遍。
  2. 对某些原生事件(change,select,beforeInput等)的升级和改造,这类事件注册时会附带注册一些依赖项,例如,给input注册了onchange事件,那么"blur", "change", "click", "focus", "input", "keydown", "keyup", "selectionchange"这些事件全都会被注册,原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。
  3. 不同浏览器事件兼容的处理
1  const nativeEventTarget = getEventTarget(nativeEvent);
2  const dispatchQueue: DispatchQueue = [];
3  // 合成事件
4  extractEvents(dispatchQueue);
5  // 触发回调
6  processDispatchQueue(dispatchQueue, eventSystemFlags);
复制代码

合成过程

  1. 根据事件类型选择插件进行合成(react将所有事件归纳进了六种插件,事件的合成由插件进行)代码地址:extractEvents
  2. 根据事件的类型,实例化不同React事件的构造函数进行合成,代码地址: SimpleEventPlugin.extractEvents
1  let EventConstructor;
2  switch (topLevelType) {
3    case DOMTopLevelEventTypes.TOP_KEY_DOWN:
4    case DOMTopLevelEventTypes.TOP_KEY_UP:
5      EventConstructor = SyntheticKeyboardEvent;
6      break;
7    // other case ...
8  }
9  const event = new EventConstructor(
10    reactName,
11    null,
12    nativeEvent,
13    nativeEventTarget,
14  );
复制代码

累积所有实例和侦听器

  1. 根据fiber节点找到对应事件的回调函数
1  function getListener() {
2    const stateNode = inst.stateNode;
3    if (stateNode === null) {
4      // Work in progress (ex: onload events in incremental mode).
5      return null;
6    }
7    const props = getFiberCurrentPropsFromNode(stateNode);
8    if (props === null) {
9      // Work in progress.
10      return null;
11    }
12    const listener = props[registrationName];
13    return listener;
14  }
复制代码
  1. 根据捕获或冒泡阶段给事件队列listeners添加回调函数
    • 先将捕获事件的回调放入listeners里
    • 如果是冒泡和捕获阶段都需要触发的事件,则放到listener的头部(unshift)
    • 如果不是,捕获阶段的事件放到listener的尾部(pop)
1  const listeners: Array<DispatchListener> = [];
2  // 需要注意一下下面这两变量都是字符串,不是布尔值
3  const bubbled = event._reactName;
4  const captured = bubbled !== null ? bubbled + 'Capture' : null;
5
6  // 是否需要模拟冒泡和捕获两个阶段,capturePhaseEvents是代码里定义的枚举值,如focus,blur
7  const shouldEmulateTwoPhase = capturePhaseEvents.has(
8    ((targetType: any): DOMTopLevelEventType),
9  );
10
11 if (bubbled !== null) {
12   // 拿到回调
13   const bubbleListener = getListener(instance, bubbled);
14   const entry = createDispatchListener(instance, bubbleListener, currentTarget);
15  
16   // push进队列
17   if (shouldEmulateTwoPhase) {
18       // 特定的事件触发时机放到前面
19      listeners.unshift(entry);
20    } else {
21      // 其他的事件追加在后面
22      listeners.push(entry);
23    }
24  }
25 }
复制代码

3. 节点一直向上遍历,对于每个节点重复执行1和2
1  let instance = targetFiber;
2  while (instance !== null) {
3    // do step 1
4    // do step 2
5    instance = instance.return;
6  }
复制代码

触发回调

现在listeners里包含了所有的侦听器,循环执行,将其分发出去。

1  if (listeners.length !== 0) {
2    dispatchQueue.push(createDispatchEntry(event, listeners));
3  }
4  
5  function processDispatchQueue(
6    dispatchQueue: DispatchQueue,
7    eventSystemFlags: EventSystemFlags,
8  ): void {
9    const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
10    for (let i = 0; i < dispatchQueue.length; i++) {
11      const {event, listeners} = dispatchQueue[i];
12      // 把listners 循环一遍依次执行
13      processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
14      // Modern event system doesn't use pooling.
15    }
16  }
复制代码

另外有一点是当其中有某一个回调执行了stopPropagation后,后续的代码就不会执行了。

1
复制代码

作者向恢进

文章分类
前端
文章标签