likes
comments
collection

【保姆级】react17 事件机制源码解析

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

写在前面

react17是一个过渡版本,最大的改动莫过于针对事件机制的重构。虽然现在react18版本已经发布了,但对事件机制这部分几乎是没有改动的,因此这里依然可以对17版本的这部分源码作一次解读。

摘自react官网的独白:

【保姆级】react17 事件机制源码解析

这里把个人觉得较重大的改动框出来了:

  1. 将事件委托给根节点而不是document;
  2. 让所有的Capture事件与浏览器捕获阶段保持一致;
  3. 移除事件池;

恕我直言,你看到的不一定是真实的

  1. 元素上的事件并不是绑定在本身;

  2. event 并不是元素本身的事件对象;

  3. 整个事件流(捕获、冒泡)过程都是 react 模拟的;

接下来先整个面试题开开胃吧,在 react16.x 版本中,如下代码执行,弹窗组件表现如何?

state={
  visible:false
}
componentDidMount(){
  document.addEventListener('click',()=>{
    this.setState({
      visible:false
    })
  })
}
handleClick = ()=>{
  this.setState({visible:true})
}
render(){
  return(
    <>
      <button onClick={this.handleClick}>
        点击打开dialog
      </button>
      {
        this.state.visible && <Dialog/>
      }
    </>
  )
}

先卖个关子,看完后面内容就自然知道了😁。

正式开始吧

事件绑定

// packages/react-dom/src/client/ReactDOMRoot.js
function createRootImpl(
  container: Container, // 项目根节点
  tag: RootTag,
  options: void | RootOptions,
) {
  if (enableEagerRootListeners) {
    const rootContainerElement =
      container.nodeType === COMMENT_NODE ? container.parentNode : container;
    // 注意此函数 监听所有支持的事件
    listenToAllSupportedEvents(rootContainerElement);
  }
  return root;
}
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (enableEagerRootListeners) {
  // allNativeEvents 这个变量是所有原生事件的集合
    allNativeEvents.forEach(domEventName => {
      if (!nonDelegatedEvents.has(domEventName)) {
        listenToNativeEvent(
          domEventName,
          false,
          ((rootContainerElement: any): Element),
          null,
        );
      }
      listenToNativeEvent(
        domEventName,
        true,
        ((rootContainerElement: any): Element),
        null,
      );
    });
  }
}

这里有一个疑问 allNativeEvents 这个变量是怎么赋值的?实际上 packages/react-dom/src/events/DOMPluginEventSystem.js 在这个文件的顶部

// packages/react-dom/src/events/DOMPluginEventSystem.js
import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';

SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

在引入插件的同时调用了插件对应的事件注册方法,关于插件的内容在后续讲解针对事件源event的处理时再来讨论。

// packages/react-dom/src/events/EventRegistry.js
export const allNativeEvents: Set<DOMEventName> = new Set();
export function registerDirectEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
) {
  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

在搞清楚 allNativeEvents 的来源后我们继续往下

// packages/react-dom/src/events/DOMPluginEventSystem.js
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 特别注意这个地方 对事件的回调函数作了一次包装 稍后在事件触发阶段再来详细聊聊
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
  let unsubscribeListener;
  if (isCapturePhaseListener) {
    unsubscribeListener = addEventCaptureListener(
        targetContainer,
        domEventName,
        listener,
      );
  } else {
    unsubscribeListener = addEventBubbleListener(
        targetContainer,
        domEventName,
        listener,
      );
  }
}

最后我们来到了真正的事件绑定的地方

/*
packages/react-dom/src/events/EventListener.js
*/
export function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, false);
  return listener;
}
export function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, true);
  return listener;
}

最后我们在根节点上绑定事件大概就是这样的:

【保姆级】react17 事件机制源码解析

以上就是事件绑定全部流程,我们来小结一下:

主要是对事件的捕获和冒泡阶段进行分别绑定,eventType 指的是事件类型,target 就是指的是我们应用程序的根节点,这里与16版本的区别主要是:

  1. 17版本是将事件委托到了根节点上,这样有利于微前端的应用。
  2. 17版本是将所有合法事件在应用程序初始化的时候都绑定上了,而16版本是按需绑定,因此在16版本react事件与原生事件有一个映射关系,比如 onChange:[blur,input,focus...]。

事件触发

在进行下面内容之前,我们最好得先有一个思维模型:

  1. 在上个阶段根节点绑定的原生事件中回调函数,其实是经过react底层包装过的,DOM事件流:捕获阶段 ===> 目标阶段 ===> 冒泡阶段,react其实是在底层模拟了这些过程。
  2. 事件执行是收集react元素上的事件监听;因此如果原生事件如果阻止了事件冒泡,那么后续的收集过程也就不执行了,自然react事件也就不执行了。

现在我们回到上面提到过的

packages/react-dom/src/events/DOMPluginEventSystem.js 文件,

来看看createEventListenerWrapperWithPriority这个方法:

// packages/react-dom/src/events/ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

先忽略事件优先级的判断,因此我们将关注点放在 dispatchEvent 这个方法,可以得知该方法是所有监听事件的回调函数,通过bind传入了额外的参数,我们来看看这个方法的具体实现:

// packages/react-dom/src/events/ReactDOMEventListener.js
export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  if (!_enabled) {
    return;
  }
// 此处是根据dom查找对应fiber的关键
  const blockedOn = attemptToDispatchEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent,
  );

  if (blockedOn === null) {
    // We successfully dispatched this event.
    if (allowReplay) {
      clearIfContinuousEvent(domEventName, nativeEvent);
    }
    return;
  }

  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    null,
    targetContainer,
  );
}

首先来看看接收的参数 domEventName、eventSystemFlags、targetContainer 这三个参数是通过上面的 bind 方法传入的,nativeEvent 则是原生的事件对象。在这个方法中执行 attemptToDispatchEvent 这里会尝试添加派发事件

// packages/react-dom/src/events/ReactDOMEventListener.js
export function attemptToDispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
  const nativeEventTarget = getEventTarget(nativeEvent);
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);
  
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
  // We're not blocked on anything.
  return null;
}

注意这个函数 getClosestInstanceFromNode:从真实dom中找到对应的fiber,具体是怎么做的呢?

// packages/react-dom/src/client/ReactDOMHostConfig.js
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}
// packages/react-dom/src/client/ReactDOMComponentTree.js
const randomKey = Math.random()
  .toString(36)
  .slice(2);
const internalInstanceKey = '__reactFiber$' + randomKey;

export function precacheFiberNode(
  hostInst: Fiber,
  node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
): void {
  (node: any)[internalInstanceKey] = hostInst;
}

其实就是在处理fiber的complete阶段中,在真实dom节点上添加了一个 internalInstanceKey 属性指向对应的fiber节点。记住这里,下文要用到。

回到事件派发,接着往下:

// packages/react-dom/src/events/DOMPluginEventSystem.js
export function dispatchEventForPluginEventSystem(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  batchedEventUpdates(() =>
    dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      ancestorInst,
      targetContainer,
    ),
  );
}

注意这里有批量更新的处理就是这里 batchedEventUpdates ,比如我们在点击事件中多次setState,在这里都是都会做一次批量处理合成一次,写到这我突然又想起了一道面试题:如何打破react的批量更新?答案是:将执行代码放在setTimeout中,但仅适用于类组件中。

接着往下:

// packages/react-dom/src/events/DOMPluginEventSystem.js
function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

在这里我们需要关注两个地方:dispatchQueue、extractEvents,dispatchQueue这个变量储存的是需要触发的事件队列。同时我们也终于看到了react合成事件的核心了:extractEvents,还记得文章的开头讲的event事件对象其实并不是原生的,而是经过react底层包装过的。

// packages/react-dom/src/events/DOMPluginEventSystem.js
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
) {
  SimpleEventPlugin.extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  const shouldProcessPolyfillPlugins =
    (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
  if (shouldProcessPolyfillPlugins) {
  // 省略部分代码...
  }
}

在这个函数中解释了,react为什么要引入事件插件的原因?因为在不同类型的事件中,比如onClick,onChange的事件对象中需要进行不同的处理(polyfill)。

// packages/react-dom/src/events/plugins/SimpleEventPlugin.js
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
): void {
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;

  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {
   // 略...
  } else {
    const accumulateTargetOnly =
      !inCapturePhase &&
      // TODO: ideally, we'd eventually add all events from
      // nonDelegatedEvents list in DOMPluginEventSystem.
      // Then we can remove this special list.
      // This is a breaking change that can wait until React 18.
      domEventName === 'scroll';
// 根据对应fiber向上递归找到整个react事件队列
    const listeners = accumulateSinglePhaseListeners(
      targetInst,
      reactName,
      nativeEvent.type,
      inCapturePhase,
      accumulateTargetOnly,
    );
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      dispatchQueue.push({event, listeners});
    }
  }
}

这里我们需要关注这几个地方:

  1. accumulateSinglePhaseListeners 函数执行结果得到元素上的所有react事件,比如onClick、onChange。
  2. 合成event事件对象构造函数 SyntheticEventCtor;

先来看accumulateSinglePhaseListeners函数:

// packages/react-dom/src/events/DOMPluginEventSystem.js
export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
): Array<DispatchListener> {
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  const reactEventName = inCapturePhase ? captureName : reactName;
  const listeners: Array<DispatchListener> = [];

  let instance = targetFiber;
  let lastHostComponent = null;

  while (instance !== null) {
    const {stateNode, tag} = instance;
    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = stateNode;
      // Standard React on* listeners, i.e. onClick or onClickCapture
      if (reactEventName !== null) {
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push(
            createDispatchListener(instance, listener, lastHostComponent),
          );
        }
      }
    }
    if (accumulateTargetOnly) {
      break;
    }
    instance = instance.return;
  }
  return listeners;
}

在真正理解这个函数之前,我们得回顾一下上面提到过的 根据dom获取对应的fiber节点(getClosestInstanceFromNode函数),这里其实就是将递归的从子fiber到祖先fiber上的props属性上依次收集react事件。

接下来我们直接看是如何生成事件对象的:

// packages/react-dom/src/events/SyntheticEvent.js
function createSyntheticEvent(Interface: EventInterfaceType) {
  function SyntheticBaseEvent(
    reactName: string | null,
    reactEventType: string,
    targetInst: Fiber,
    nativeEvent: {[propName: string]: mixed},
    nativeEventTarget: null | EventTarget,
  ) {
    this._reactName = reactName;
    this._targetInst = targetInst;
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = nativeEventTarget;
    this.currentTarget = null;

    for (const propName in Interface) {
      if (!Interface.hasOwnProperty(propName)) {
        continue;
      }
      const normalize = Interface[propName];
      if (normalize) {
        this[propName] = normalize(nativeEvent);
      } else {
        this[propName] = nativeEvent[propName];
      }
    }

    const defaultPrevented =
      nativeEvent.defaultPrevented != null
        ? nativeEvent.defaultPrevented
        : nativeEvent.returnValue === false;
    if (defaultPrevented) {
      this.isDefaultPrevented = functionThatReturnsTrue;
    } else {
      this.isDefaultPrevented = functionThatReturnsFalse;
    }
    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  Object.assign(SyntheticBaseEvent.prototype, {
    preventDefault: function() {
      this.defaultPrevented = true;
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.preventDefault) {
        event.preventDefault();
        // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.returnValue !== 'unknown') {
        event.returnValue = false;
      }
      this.isDefaultPrevented = functionThatReturnsTrue;
    },

    stopPropagation: function() {
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.stopPropagation) {
        event.stopPropagation();
        // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.cancelBubble !== 'unknown') {
        // 兼容ie
        event.cancelBubble = true;
      }

      this.isPropagationStopped = functionThatReturnsTrue;
    },
    persist: function() {
      // Modern event system doesn't use pooling.
    },
    isPersistent: functionThatReturnsTrue,
  });
  return SyntheticBaseEvent;
}

这是一个工厂函数,首先定义了一个基础的事件对象类:SyntheticBaseEvent,在此原型链上,添加了preventDefault、stopPropagation方法来对应原生事件。仔细看这两个方法的实现过程不难看出里面对ie浏览器做了兼容处理,此处应该给react鼓掌👏🏻。同时也解释了react费劲巴拉的搞出一个事件机制的原因:试图抹平程序运行在各浏览器的差异。另外此函数接受了一个名为Interface的参数,目的是用作在实例化 SyntheticBaseEvent 类时,将不同的事件类型需要的属性挂到this上去。比如:

const EventInterface = {
  eventPhase: 0,
  bubbles: 0,
  cancelable: 0,
  timeStamp: function(event) {
    return event.timeStamp || Date.now();
  },
  defaultPrevented: 0,
  isTrusted: 0,
};
export const SyntheticEvent = createSyntheticEvent(EventInterface);

const UIEventInterface: EventInterfaceType = {
  ...EventInterface,
  view: 0,
  detail: 0,
};
export const SyntheticUIEvent = createSyntheticEvent(UIEventInterface);

聊完了合成事件对象,接下来就是执行我们的事件监听了,在这里最主要的就是模拟事件捕获和冒泡:

// packages/react-dom/src/events/DOMPluginEventSystem.js
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      // 执行用户定义的react函数
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      // 执行用户定义的react函数
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

上文我们讲过事件是通过自下而上的方式收集的,这里很明显的可以看出通过反向遍历事件队列来模拟了事件的捕获,然后再次通过正向遍历的方式模拟事件的冒泡。同时在两次遍历的过程中,如果在某次事件执行了 e.stopPropagation(),实际上调用的是合成event对象中自定义的 stopPropagation 方式,同时isPropagationStopped()返回结果为true,自然在此处的遍历就阻止了事件的继续传播。自此,我们整个事件机制就已经串起来了。

现在我们也可以回答开篇提到的那道面试了:

首先弹框永远不会显示,原因是react16版本事件是委托到document上的,当点击按钮后visible置为true,但事件冒泡到document后又将visible置为了false。讲到这有些同学可能会说,那还不简单,在按钮点击事件上加一个e.stopPropagation()不就好了?答案是否定的,e.stopPropagation()只能阻止事件向上冒泡,而在这里按钮事件是被委托到document上的,它们是同级而不是上下级。只能这么解决:e.nativeEvent.stopImmediatePropagation(),这个方法可以阻止事件同级传播。

但在17版本就不会存在这个问题,直接用e.stopPropagation()就可以解决问题,因为17版本事件是被委托到了根节点而不是document。

总结

简单来说整个react17的事件机制,可分为两个流程:

  1. 事件绑定

在这个过程中,首先在react程序初始化的时候就往根节点上注册了所有合法的原生事件,并通过 dispatchEvent 函数作为事件的统一回调函数;

  1. 事件触发

上面定义的 dispatchEvent 事件回调函数被触发,分别执行了 从真实dom上查找对应的fiber、事件批量更新的处理、根据不同的事件类型合成不同的事件对象(event)包括重新定义了 stopPropagation preventDefault 并做了ie兼容处理、在对应的fiber节点自下而上递归遍历出react事件、最后根据正向、反向遍历react事件队列分别模拟出事件的捕获与冒泡阶段。

以上就是文章的全部内容,欢迎各位大佬的指正。码字不易,还请看官动动小手点赞评论😁