likes
comments
collection
share

[React 源码] React18 合成事件 [2.5k 字 - 阅读时长10min]

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

代码都来自 React 18 源码, 大家可以放心食用

读完收获

  1. 学会 Dom 事件流

  2. 理解 事件委托

  3. 掌握 React 合成事件原理

Dom 事件流

事件流包含三个阶段:

  1. 事件捕获阶段
  2. 目标阶段
  3. 事件冒泡阶段

首先发生的是事件捕获,然后是实际的目标接收到事件,最后阶段是事件冒泡阶段。

<html>
    <body>
        <div>
            <button></button>
        </div>
    </body>
</html>

[React 源码]  React18  合成事件  [2.5k 字 - 阅读时长10min]

事件捕获阶段

事件捕获是先由最上层节点 document 先接收事件, 然后向下传播到具体的节点 document->body->div->button

目标阶段

在目标节点上触发,称为目标阶段

//w3c浏览器:event.target 
//IE: event.srcElement

let target = event.target || event.srcElement;

事件冒泡阶段

从目标节点开始 (这里是 button),然后逐级向上传播 button->div->body->document

addEventListener


// useCapture 默认是 fales,当参数是true,则在捕获阶段绑定函数,反之,在冒泡阶段绑定函数,
element.addEventListener(event, function, useCapture)

阻止冒泡

  // IE
  window.event.cancelBubble = true;
  // w3c
  event.stopPropagation();

事件代理

事件代理又称之为事件委托, 事件代理是把原本需要绑定在子元素的事件委托给父元素,让父元素负责事件监听和处理。

事件代理的好处是有两点:

第一点:可以大量节省内存占用,减少事件注册事件。

第二点:当新增子对象时无需再次对其绑定

为什么父元素能做到事件代理呢? 笔者认为有两点:

第一点 :事件冒泡到父元素,父元素可以订阅到冒泡事件。

第二点:可以通过 event.target 得到目标节点。不然, 父元素怎么针对不同的子节点,进行定制化事件代理。

React 合成事件原理

下面这段代码在 React 18合成事件的打印结果是:

/**
document原生捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
 */

[React 源码]  React18  合成事件  [2.5k 字 - 阅读时长10min]

const App = () => {
  const divRef = useRef();
  const pRef = useRef();

  const parentBubble = () => {
    console.log("父元素React事件冒泡");
  };
  const childBubble = () => {
    console.log("子元素React事件冒泡");
  };
  const parentCapture = () => {
    console.log("父元素React事件捕获");
  };
  const childCapture = () => {
    console.log("子元素React事件捕获");
  };

  useEffect(() => {
    divRef.current.addEventListener(
      "click",
      () => {
        console.log("父元素原生捕获");
      },
      true
    );
    divRef.current.addEventListener("click", () => {
      console.log("父元素原生冒泡");
    });

    pRef.current.addEventListener(
      "click",
      () => {
        console.log("子元素原生捕获");
      },
      true
    );
    pRef.current.addEventListener("click", () => {
      console.log("子元素原生冒泡");
    });

    document.addEventListener(
      "click",
      () => {
        console.log("document原生捕获");
      },
      true
    );
    document.addEventListener("click", () => {
      console.log("document原生冒泡");
    });
  }, []);

  return (
    <div ref={divRef} onClick={parentBubble} onClickCapture={parentCapture}>
      <p ref={pRef} onClick={childBubble} onClickCapture={childCapture}>
        事件执行顺序
      </p>
    </div>
  );
};

Mout 阶段: 点击之前

Mout 阶段总结:点击之前 就是在 root 容器上监听了 JavaScript 所有原生事件的冒泡和捕获。

第一:通过调用 SimpleEventPlugin.registerEvents 插件函数来注册事件(在项目当中的 index.tsx 中 调用 createRoot 函数 之前就去注册了。)

import {allNativeEvents} from './EventRegistry';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';

SimpleEventPlugin.registerEvents();

SimpleEventPlugin.registerEvents 函数:处理,加工原始事件名字为 React 事件名字,比如 将 click 变为 onClick, 然后调用 registerSimpleEvent 函数 将原始事件名字和 React 事件名字 建立 Map 映射关系,比如 Map {click: onClick}

// 所有原生事件
const simpleEventPluginEvents = [
  'abort',
  'auxClick',
  'click',
   // 剩余所有原生事件
];

//  React 事件和原始事件的映射 Map
export const topLevelEventsToReactNames = new Map();

SimpleEventPlugin.registerEvents 
export function registerSimpleEvents() {
  for (let i = 0; i < simpleEventPluginEvents.length; i++) {
    const eventName = ((simpleEventPluginEvents[i]: any): string);
    const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
    const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
    registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
  }
}

function registerSimpleEvent(domEventName, reactName) {
  topLevelEventsToReactNames.set(domEventName, reactName);
  registerTwoPhaseEvent(reactName, [domEventName]);
}

第四:原始事件名字和 React 事件名字 建立 Map 映射关系 之后,在 registerSimpleEvent 函数 调用 registerTwoPhaseEvent 函数, 在 registerTwoPhaseEvent 函数中调用 registerDirectEvent 函数 结果是将所有原生事件名字 加入到 allNativeEvents 数组当中去,比如:[click, dbclick]。调用两次是因为要在 registrationNameDependencies = {} 映射冒泡和捕获。比如:{onClick: click, onClickCapture: click}

为什么 dependencies 是数组,因为 一个 React 事件可能会对应多个原生事件。

export function registerTwoPhaseEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
): void {
  registerDirectEvent(registrationName, dependencies);
  registerDirectEvent(registrationName + 'Capture', dependencies);
}

// {onClick: click, onClickCapture: click}
export const registrationNameDependencies = {};


export function registerDirectEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
) {
  registrationNameDependencies[registrationName] = dependencies;
  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

第五: 在项目当中的 index.tsx 中 调用 createRoot 函数

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)

root.render(<App/>)

第六:在 createRoot 函数 当中 调用 listenToAllSupportedEvents函数,并创建 FiberRoot

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions
): RootType {
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks
  );

  listenToAllSupportedEvents(rootContainerElement);
  return new ReactDOMRoot(root);
}

第七:调用 listenToAllSupportedEvents函数, 见名知意, 监听所有原生事件。 来看看这里是怎么监听。

经过前五步,已经将所有的原生事件都放到了 allNativeEvents 数组中。遍历 allNativeEvents 数组,调用 listenToNativeEvent 函数。

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  allNativeEvents.forEach(domEventName => {
    if (domEventName !== 'selectionchange') {
      if (!nonDelegatedEvents.has(domEventName)) {
        listenToNativeEvent(domEventName, false, rootContainerElement);
      }
      listenToNativeEvent(domEventName, true, rootContainerElement);
    }
  });
}

第八:listenToNativeEvent 调用了 addTrappedEventListener 函数。

第九:addTrappedEventListener 函数 当中,首先通过 createEventListenerWrapperWithPriority 函数 创建了 listenr 监听函数,这个监听函数很重要,注意看!!!!

listenr 监听函数通过 dispatchEvent 去 bind 绑定一些重要参数,返回的一个函数。

这样每次事件触发都可以调用 dispatchEvent 并且携带一些固定的参数。

export function createEventListenerWrapper(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  return dispatchEvent.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

然后调用 addEventBubbleListener addEventCaptureListener 函数。

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean
) {
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags
  );

  unsubscribeListener = addEventBubbleListener(
    targetContainer,
    domEventName,
    listener
  );
  unsubscribeListener = addEventCaptureListener(
    targetContainer,
    domEventName,
    listener
  );
}

第十:调用 addEventBubbleListener addEventCaptureListener 函数,真相大白 😀,原来结果就是在 root 根容器上绑定了原生事件的冒泡和捕获事件。

export function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
) {
  target.addEventListener(eventType, listener, false);
  retu电击 listener;
}

export function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, true);
  return listener;
}

Mout 阶段总结:点击之前 就是在 root 容器上监听了 JavaScript 所有原生事件的冒泡和捕获。

点击触发

总结:提取所有事件监听的处理函数放到 dispatchQueue 当中。然后,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。

上文提到,所有事件的触发,都绑定到了 dispachtEvent 函数上,相当于:

div.addEventListener("click", dispatchEvent.bind(null,
    click,
    eventSystemFlags,
    div#root))
    
    
div.addEventListener("dbclick", dispatchEvent.bind(null,
    dbclick,
    eventSystemFlags,
    div#root))

第一:点击触发,调用 dispatchEvent 函数。函数做了至关重要的两件事情

第一件事情是:通过调用 extractEvents 函数, 提取所有监听的处理函数放到 dispatchQueue 当中。

第二件事情是:通过调用 processDispatchQueue 函数,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。

基于这两件事情,我们看看 Reect 是怎么完成的?

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);
}

第一件事情是:调用 extractEvents 函数,首先根据不同的事件类型名字,去获取不同类别的事件对象 event,也就是React 传递给开发者的 事件对象 event。

然后,在该函数当中调用 accumulateSinglePhaseListeners函数。这个 accumulateSinglePhaseListeners函数 做的事情就是 从当前的 Fiebr 节点开始一直向上遍历,找到路径上 fiber 节点的所有绑定事件函数比如:onClick, onClickCapture。经过包装成为 Dispatch 返回给 listeners。

  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent
  );

然后将 Dispatch 和 匹配的 event 对象,封装成一个对象,加入到 dispatdchQueue 当中去,

 dispatchQueue.push({ event, listeners });
 

extractEvents 函数整体实现

function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget
): void {
  const reactName = topLevelEventsToReactNames.get(domEventName);
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
  
  // 首先根据不同的事件类型名字,去获取不同类别的事件对象 event,也就是React 传递给开发者的 事件对象 event。
  switch (domEventName) {
    case "click":
    case "mousemove":
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case "drop":
      SyntheticEventCtor = SyntheticDragEvent;
      break;
    default:
      break;
  }
 // 通过调用 `extractEvents` 函数, 提取所有监听的处理函数放到 `dispatchQueue` 当中。
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent
  );

  if (listeners.length > 0) {
    const event: ReactSyntheticEvent = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget
    );
    //  Dispatch 和 匹配的 event 对象,封装成一个对象,加入到 `dispatdchQueue` 当中去,
    dispatchQueue.push({ event, listeners });
  }
}

第二件事情是:通过调用 processDispatchQueue 函数,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。比如:捕获阶段,从最高层节点向下传播,而加入到队列的顺序 是从目标节点开始向上加入的,所以要想模拟捕获,就需要从最后一个节点开始 倒序执行。要想模拟冒泡,就需要从 第一个节点开始,正序执行。

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];
      // 这里的判断是因为,React 重写了 阻止冒泡的方法,可以通过 isPropagationStopped 来判断是否阻止
      //了冒泡,如果阻止了冒泡,则立即返回。
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      
      // 执行事件监听函数,传入 Event 对象。
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    for (let i = 0; i < dispatchListeners.length; i++) {
      const { instance, currentTarget, listener } = dispatchListeners[i];
       // 这里的判断是因为,React 重写了阻止冒泡的方法,可以通过 isPropagationStopped 来判断是否阻止
       //了冒泡,如果阻止了冒泡,则立即返回。
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      // 执行事件监听函数,传入Event 对象。
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

总结:提取所有事件监听的处理函数放到 dispatchQueue 当中。然后,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。

至此,事件触发阶段完成