Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 源码解析:合成事件 #20

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

React 源码解析:合成事件 #20

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

React 源码解析:合成事件

React 的事件是通过封装原生事件而得到的合成事件。合成事件无论在性能、兼容上都比原生事件具有优势,而且合成事件在 React 体系中占有非常重要的作用,比如对于 setState

概念

合成事件机制是:通过拦截封装原生事件,都绑定到 document 元素上,统一管理,绑定,并通过事件冒泡方式传递到绑定的 document 元素上,触发同一类型事件。比如:

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 0,
        count: 0,
      }
    }

    clickButton = (event) => {
      console.log('合成事件 onClick');
    }

    render () {
      return (
        [
          <div key="div">{this.state.data}</div>,
          <button id="btn" key="btn" onClick={this.clickButton}>click me</button>
        ]
      )
    }
}

虽说我们在 button 元素上书写了 clickButton 函数,但是 clickButton 函数实际是绑定到 document 元素上的。当我们点击 button 元素时,通过冒泡到 document 元素,触发 clickButton 函数。

有一个问题是:如果在原生事件中阻止冒泡,那么 React 合成事件是不会触发的。

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 0,
        count: 0,
      }
    }
    
    componentDidMount() {
    	document.getElementById('btn').addEventListener('click', (e) => {
    		console.log('原生事件 click');
    		e.stopPropagation();
    	});
    }

    clickButton = (event) => {
      console.log('合成事件 onClick');
    }

    render () {
      return (
        [
          <div key="div">{this.state.data}</div>,
          <button id="btn" key="btn" onClick={this.clickButton}>click me</button>
        ]
      )
    }
}

因为在 button 元素的原生事件中阻止了冒泡,上面只能执行原生事件而 button 元素的 clickButton 函数不会执行。

合成事件的注册

注册流程如下:

  1. 通过 createElement 函数得到 DOM 属性 Props。
  2. 调用 setInitialDOMProperties 函数,判断属性是否为事件类型。
  3. 如果是事件则进入 ensureListeningTo 函数中,找到 document 对象。
  4. 继续调用 listenTo 函数,检查 document 是否绑定了同类事件。如果没有,进入 trapCapturedEvent 或者 trapBubbledEvent 函数。这里以 click 事件为例,会进入 trapBubbledEvent
  5. trapBubbledEvent 函数中首先提取 dispatch 函数(如果是异步就是 dispatchInteractiveEvent,同步是 dispatchEvent,如果 React 对事件做了支持就会做异步处理,否则采用原生事件也就是同步)。
  6. 进入 addEventBubbleListener 函数,在 document 中绑定 dispatch 函数。

最后在 document 注册的是 dispatch 函数,没有把事件的回调函数保存起来。实际上回调函数是存在 fiber 节点上的,是在事件发生后获取到的,具体在合成事件触发流程中介绍。

合成事件触发流程

大概流程如下:

  1. 事件冒泡到 document 元素上,如果是异步,会比同步多调用几个函数:dispatchInteractiveEventinteractiveUpdatesinteractiveUpdates$1(后缀带 $1 都是打包过后的函数,原函数是其他函数。这里是 interactiveUpdates 定义在 ReactFiberScheduler.js 文件中) 。
  2. 触发 dispatchEvent 函数进行事件分发。在 dispatchEvent 函数中有一个重要的作用是生成 bookKeeping 对象。
  3. 随后调用 batchedUpdatesbatchedUpdates$1batchedUpdates 定义在 ReactFiberScheduler.js 文件中) 两个函数,是关于批量更新的。进入 handleTopLevel 函数。
  4. 调用 runExtractedEventsInBatch 函数,从这里开始进入合成事件对象生成逻辑。然后调用 extractEventsaccumulateInto
  5. 调用 runEventsInBatchforEachAccumulatedexecuteDispatchesAndReleaseTopLevelexecuteDispatchesAndRelease 函数。
  6. 然后会走到 executeDispatchesInOrder 函数中,后面会继续调用 executeDispatchinvokeGuardedCallbackAndCatchFirstErrorinvokeGuardedCallbackinvokeGuardedCallbackImpl$1(invokeGuardedCallbackDev 定义在 invokeGuardedCallbackImpl.js 文件中)。此时合成事件执行完毕,其中在 invokeGuardedCallbackDev 函数中会执行事件的回调函数。

合成事件触发的流程已经梳理完成,但是还有几点需要单独拎出来说。

合成事件对象生成

合成事件对象是经过封装的。

class App extends React.Component {
    constructor() {
      super();
      this.state = {
        data: 0,
        count: 0,
      }
    }

    clickButton = (event) => {
      console.log(event);
    }

    render () {
      return (
        [
          <div key="div">{this.state.data}</div>,
          <button id="btn" key="btn" onClick={this.clickButton}>click me</button>
        ]
      )
    }
}

clickButton 回调函数的 event 结构如下:

在 React 对合成事件做了介绍:

SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()

如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可

其中提到了事件池的概念:

SyntheticEvent 是合并而来。这意味着 SyntheticEvent 对象可能会被重用,而且在事件回调函数被调用后,所有的属性都会无效。出于性能考虑,你不能通过异步访问事件

function onClick(event) {
console.log(event); // => nullified object.
console.log(event.type); // => "click"
const eventType = event.type; // => "click"

setTimeout(function() {
 console.log(event.type); // => null
 console.log(eventType); // => "click"
}, 0);

// 不起作用,this.state.clickEvent 的值将会只包含 null
this.setState({clickEvent: event});

// 你仍然可以导出事件属性
this.setState({eventType: event.type});
}

下面探讨在代码层面合成事件的生成。

在合成事件触发时,会调用 runExtractedEventsInBatch 函数,在 runExtractedEventsInBatch 函数中又调用了 extractEvents 函数:

function extractEvents(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
  let events = null;
  for (let i = 0; i < plugins.length; i++) {
    // Not every plugin in the ordering may be loaded at runtime.
    const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
    if (possiblePlugin) {
      const extractedEvents = possiblePlugin.extractEvents(
        topLevelType,
        targetInst,
        nativeEvent,
        nativeEventTarget,
      );
      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }
  return events;
}

extractEvents 函数就是生成 event 的地方,在具体介绍前需要搞清楚 数组 plugins 的来源。

extractEvents 函数中,数组 plugins 是在初始化时得到的,初始化流程如下:

injectEventPluginOrder(通过打包后的函数 injection.injectEventPluginOrder 调用)
injectEventPluginsByName(通过打包后的函数 injection.injectEventPluginsByName 调用)
recomputePluginOrdering

通过调试可以发现数组 plugins 数据都来自于 injection.injectEventPluginsByName 函数的参数:

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

SimpleEventPlugin 参数为例:SimpleEventPlugin 来自 SimpleEventPlugin.js 文件:

const SimpleEventPlugin: PluginModule<MouseEvent> & {
  isInteractiveTopLevelEventType: (topLevelType: TopLevelType) => boolean,
} = {
  eventTypes: eventTypes,

  isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean {
    const config = topLevelEventsToDispatchConfig[topLevelType];
    return config !== undefined && config.isInteractive === true;
  },

  extractEvents: function(
    topLevelType: TopLevelType,
    targetInst: null | Fiber,
    nativeEvent: MouseEvent,
    nativeEventTarget: EventTarget,
  ): null | ReactSyntheticEvent {
    ...
  },
};

回到 extractEvents 函数中,其中有下面这段代码:

const extractedEvents = possiblePlugin.extractEvents(
  topLevelType,
  targetInst,
  nativeEvent,
  nativeEventTarget,
);

possiblePlugin 就是injection.injectEventPluginsByName 调用时的参数,这里以SimpleEventPlugin 为例。possiblePlugin.extractEvents 四个参数分别是:事件类型、fiber 节点、原生事件对象、DOM 节点。

SimpleEventPlugin 对象中的 extractEvents 函数有下面这段代码:

const event = EventConstructor.getPooled(
  dispatchConfig,
  targetInst,
  nativeEvent,
  nativeEventTarget,
);

其中 EventConstructor 是根据不同事件类型得到不同结果。以 click 类型为例,EventConstructor 就是 SyntheticMouseEventSyntheticMouseEvent 最终会继承基类 SyntheticEvent(其他事件类型一致)。在 SyntheticEvent 中会发现封装了 preventDefaultstopPropagation 和浏览器原生事件相同的接口,另外还包括 persist

最后通过 EventConstructor.getPooled 返回值得到 event,然后在经过一些函数处理最终得到合成事件 event。

回调函数的查找

在前面大概分析了合成事件对象的生成流程,在这个过程中有一个重要的步骤是查找回调函数。查找回调函数是在合成事件生成的流程中的(这里以 SimpleEventPlugin 为例),具体是从 extractEvents 中的 accumulateTwoPhaseDispatches 函数开始的:

accumulateTwoPhaseDispatches
forEachAccumulated
accumulateTwoPhaseDispatchesSingle
traverseTwoPhase
accumulateDirectionalDispatches
getListener

查找回调函数大概流程如上。

因为回调函数存储在 fiber 节点上,所以查找是根据事件类型和 DOM 节点找到相关 fiber 节点进而得到回调函数并赋值到合成事件的 _dispatchListeners 属性上。

React 会遍历冒泡经过的所有 DOM 节点,找出存在相同事件类型的回调函数。具体见 traverseTwoPhaseaccumulateDirectionalDispatches 两个函数:

export function traverseTwoPhase(inst, fn, arg) {
  const path = [];
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  let i;
  for (i = path.length; i-- > 0; ) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

traverseTwoPhase 函数中,首先找出所有祖先节点,然后 for 循环遍历节点,调用 fn 也就是 accumulateDirectionalDispatches 函数。

function accumulateDirectionalDispatches(inst, phase, event) {
  if (__DEV__) {
    warningWithoutStack(inst, 'Dispatching inst must not be null');
  }
  const listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    event._dispatchListeners = accumulateInto(
      event._dispatchListeners,
      listener,
    );
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

如果有回调函数就存储在 event._dispatchListeners 上,如果相同类型事件的回调函数不止一个,那么 event._dispatchListeners 就是数组类型。

合成事件对 setState 影响

合成事件对 setState 的影响主要就是 isBatchingUpdates 变量的赋值和 try finally 结构。在合成事件触发流程中,有两个函数对这两点有影响:interactiveUpdatesbatchedUpdates 。以点击为例,在点击事件发生后,执行回调函数之前会先后调用这两个函数:

function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
  // If there are any pending interactive updates, synchronously flush them.
  // This needs to happen before we read any handlers, because the effect of
  // the previous event may influence which handlers are called during
  // this event.
  if (
    !isBatchingUpdates &&
    !isRendering &&
    lowestPriorityPendingInteractiveExpirationTime !== NoWork
  ) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPriorityPendingInteractiveExpirationTime, false);
    lowestPriorityPendingInteractiveExpirationTime = NoWork;
  }
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return runWithPriority(UserBlockingPriority, () => {
      return fn(a, b);
    });
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

进入 interactiveUpdates 函数时,首先将 isBatchingUpdates 状态存起来:const previousIsBatchingUpdates = isBatchingUpdates;,然后:isBatchingUpdates = true;。继续进入 try 语句中,会来到 batchedUpdates 函数中:

function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return fn(a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

此时 isBatchingUpdates 状态为 true,所以 previousIsBatchingUpdates = true。然后又将isBatchingUpdates 的值置为 true。在讲解 setState 时,最终都会进入到 requestWork 函数中,因为 isBatchingUpdates 为 true,requestWork 函数返回,不继续下面的更新。

当回调函数执行完成之后,回到 batchedUpdates 函数的 finally 语句中,因为 previousIsBatchingUpdates 等于 true,所以不会进入 if 语句,在 batchedUpdates 函数中不会执行 performSyncWork

然后会继续返回到 interactiveUpdates 函数的 finally 语句中,因为进入此函数时设置 previousIsBatchingUpdates 为 false,而且 isRendering 为 false,因此调用 performSyncWork 函数更新。会再次进入 requestWork 函数中,但是此时 isBatchingUpdates 为 false 了,会正常更新。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant