关于前端:React-事件系统工作原理

30次阅读

共计 6523 个字符,预计需要花费 17 分钟才能阅读完成。

本文作者: 江水

前言

React 为咱们提供了一套虚构的事件零碎,这套虚构事件零碎是如何工作的,笔者对源码做了一次梳理,整顿了上面的文档供大家参考。

在 React 事件介绍 中介绍了合成事件对象以及为什么提供合成事件对象,次要起因是因为 React 想实现一个全浏览器的框架,为了实现这种指标就须要提供全浏览器一致性的事件零碎,以此抹平不同浏览器的差别。

合成事件对象很有意思,一开始听名字会感觉很奇怪,看到英文名更奇怪 SyntheticEvent,实际上合成事件的意思就是应用原生事件合成一个 React 事件,例如应用原生 click 事件合成了 onClick 事件,应用原生 mouseout 事件合成了 onMouseLeave 事件,原生事件和合成事件类型大部分都是一一对应,只有波及到兼容性问题时咱们才须要应用不对应的事件合成。合成事件并不是 React 的独创,在 iOS 上遇到的 300ms 问题而引入的 fastclick 就应用了 touch 事件合成了 click 事件,也算一种合成事件的利用。

理解了 React 事件是合成事件之后咱们对待事件的角度就会有所不同,例如咱们常常在代码中写的这种代码

<button onClick={handleClick}>
 Activate Lasers
</button>

咱们曾经晓得这个 onClick 只是一个合成事件而不是原生事件,那这段时间到底产生了什么?原生事件和合成事件是如何对应起来的?
下面的代码看起来很简洁,实际上 React 事件零碎工作机制比起下面要简单的多,脏活累活全都在底层解决了,几乎框架劳模。其工作原理大体上分为两个阶段

  1. 事件绑定
  2. 事件触发

上面就一起来看下这两个阶段到底是如何工作的,这里次要从源码层剖析,并以 16.13 源码中内容为基准。

1. React 是如何绑定事件的 ?

React 既然提供了合成事件,就须要晓得合成事件与原生事件是如何对应起来的,这个对应关系寄存在 React 事件插件中 EventPlugin,事件插件能够认为是 React 将不同的合成事件处理函数封装成了一个模块,每个模块只解决本人对应的合成事件,这样不同类型的事件品种就能够在代码上解耦,例如针对onChange 事件有一个独自的 LegacyChangeEventPlugin 插件来解决,针对onMouseEnteronMouseLeave 应用 LegacyEnterLeaveEventPlugin 插件来解决。

为了晓得合成事件与原生事件的对应关系,React 在一开始就将事件插件全副加载进来,这部分逻辑在 ReactDOMClientInjection 代码如下

injectEventPluginsByName({
 SimpleEventPlugin: LegacySimpleEventPlugin,
 EnterLeaveEventPlugin: LegacyEnterLeaveEventPlugin,
 ChangeEventPlugin: LegacyChangeEventPlugin,
 SelectEventPlugin: LegacySelectEventPlugin,
 BeforeInputEventPlugin: LegacyBeforeInputEventPlugin
});

注册完上述插件后,EventPluginRegistry (老版本代码里这个模块唤作 EventPluginHub) 这个模块里就初始化好了一些全局对象,有几个对象比拟重要,能够独自说一下。

第一个对象是 registrationNameModule,它蕴含了 React 事件到它对应的 plugin 的映射,大抵长上面这样,它蕴含了 React 所反对的所有事件类型,这个对象最大的作用是判断一个组件的 prop 是否是事件类型,这在解决原生组件的 props 时候将会用到,如果一个 prop 在这个对象中才会被当做事件处理。

{
 onBlur: SimpleEventPlugin,
 onClick: SimpleEventPlugin,
 onClickCapture: SimpleEventPlugin,
 onChange: ChangeEventPlugin,
 onChangeCapture: ChangeEventPlugin,
 onMouseEnter: EnterLeaveEventPlugin,
 onMouseLeave: EnterLeaveEventPlugin,
 ...
}

第二个对象是 registrationNameDependencies,这个对象长上面几个样子

{onBlur: ['blur'],
 onClick: ['click'],
 onClickCapture: ['click'],
 onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
 onMouseEnter: ['mouseout', 'mouseover'],
 onMouseLeave: ['mouseout', 'mouseover'],
 ...
}

这个对象即是一开始咱们说到的合成事件到原生事件的映射,对于 onClickonClickCapture 事件,只依赖原生 click 事件。然而对于 onMouseLeave它却是依赖了两个mouseoutmouseover,这阐明这个事件是 React 应用 mouseoutmouseover 模仿合成的。正是因为这种行为,使得 React 可能合成一些哪怕浏览器不反对的事件供咱们代码里应用。

第三个对象是 plugins,这个对象就是下面注册的所有插件列表。

plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];

看完下面这些信息后咱们再反过头来看下一个一般的 EventPlugin 长什么样子。一个 plugin 就是一个对象,这个对象蕴含了上面两个属性

// event plugin
{
 eventTypes, // 一个数组,蕴含了所有合成事件相干的信息,包含其对应的原生事件关系
 extractEvents: // 一个函数,当原生事件触发时执行这个函数
}

理解下面这这些信息对咱们剖析 React 事件工作原理将会很有帮忙,上面开始进入事件绑定阶段。

  1. React 执行 diff 操作,标记出哪些 DOM 类型 的节点须要增加或者更新。

  1. 当检测到须要创立一个节点或者更新一个节点时,应用 registrationNameModule 查看一个 prop 是不是一个事件类型,如果是则执行下一步。

  1. 通过 registrationNameDependencies 查看这个 React 事件依赖了哪些 原生事件类型

  1. 查看这些一个或多个原生事件类型有没有注册过,如果有则疏忽。

  1. 如果这个原生事件类型没有注册过,则注册这个原生事件到 document 上,回调为 React 提供的 dispatchEvent 函数。


下面的阶段阐明:

  1. 咱们将所有事件类型都注册到 document 上。
  2. 所有原生事件的 listener 都是 dispatchEvent 函数。
  3. 同一个类型的事件 React 只会绑定一次原生事件,例如无论咱们写了多少个onClick,最终反馈在 DOM 事件上只会有一个listener
  4. React 并没有将咱们业务逻辑里的 listener 绑在原生事件上,也没有去保护一个相似 eventlistenermap 的货色寄存咱们的listener

由 3,4 条规定能够得出,咱们业务逻辑的 listener 和理论 DOM 事件压根就没关系,React 只是会确保这个原生事件可能被它本人捕捉到,后续由 React 来派发咱们的事件回调,当咱们页面产生较大的切换时候,React 能够什么都不做,从而免去了去操作 removeEventListener 或者同步 eventlistenermap 的操作,所以其执行效率将会大大提高,相当于全局给咱们做了一次事件委托,即使是渲染大列表,也不必开发者关怀事件绑定问题。

2. React 是如何触发事件的?

咱们晓得因为所有类型品种的事件都是绑定为 React 的 dispatchEvent 函数,所以就能在全局解决一些通用行为,上面就是整个行为过程。

export function dispatchEventForLegacyPluginEventSystem(
 topLevelType: DOMTopLevelEventType,
 eventSystemFlags: EventSystemFlags,
 nativeEvent: AnyNativeEvent,
 targetInst: null | Fiber,
): void {
 const bookKeeping = getTopLevelCallbackBookKeeping(
 topLevelType,
 nativeEvent,
 targetInst,
 eventSystemFlags
 );
 try {
 // Event queue being processed in the same cycle allows
 // `preventDefault`.
 batchedEventUpdates(handleTopLevel, bookKeeping);
 } finally {releaseTopLevelCallbackBookKeeping(bookKeeping);
 }
}

bookKeeping为事件执行时组件的层级关系存储,也就是如果在事件执行过程中产生组件构造变更,并不会影响事件的触发流程。
整个触发事件流程如下:

  1. 任意一个事件触发,执行 dispatchEvent 函数。
  2. dispatchEvent 执行 batchedEventUpdates(handleTopLevel),batchedEventUpdates 会关上批量渲染开关并调用 handleTopLevel
  3. handleTopLevel 会顺次执行 plugins 里所有的事件插件。
  4. 如果一个插件检测到本人须要解决的事件类型时,则解决该事件。

对于大部分事件而言其解决逻辑如下,也即 LegacySimpleEventPlugin 插件做的工作

  1. 通过原生事件类型决定应用哪个合成事件类型(原生 event 的封装对象,例如 SyntheticMouseEvent)。
  2. 如果对象池里有这个类型的实例,则取出这个实例,笼罩其属性,作为本次派发的事件对象(事件对象复用),若没有则新建一个实例。

  1. 从点击的原生事件中找到对应 DOM 节点,从 DOM 节点中找到一个最近的 React 组件实例,从而找到了一条由这个实例父节点一直向上组成的链,这个链就是咱们要触发合成事件的链,(只蕴含原生类型组件,diva 这种原生组件)。

  1. 反向触发这条链,父 -> 子,模仿捕捉阶段,触发所有 props 中含有 onClickCapture 的实例。

  1. 正向触发这条链,子 -> 父,模仿冒泡阶段,触发所有 props 中含有 onClick 的实例。


这几个阶段阐明了上面的景象:

  1. React 的合成事件只能在事件周期内应用,因为这个对象很可能被其余阶段复用,如果想长久化须要手动调用event.persist() 通知 React 这个对象须要长久化。(React17 中被废除)
  2. React 的冒泡和捕捉并不是真正 DOM 级别的冒泡和捕捉
  3. React 会在一个原生事件里触发所有相干节点的 onClick 事件,在执行这些 onClick 之前 React 会关上批量渲染开关,这个开关会将所有的 setState 变成异步函数。
  4. 事件只针对原生组件失效,自定义组件不会触发 onClick

3. 从 React 的事件零碎中咱们学到了什么

  1. React16 将原生事件都绑定在 document 上.

这点很好了解,React 的事件实际上都是在 document 上触发的。

  1. 咱们收到的 event 对象为 React 合成事件,event 对象在事件之外不能够应用

所以上面就是谬误用法

function onClick(event) {setTimeout(() => {console.log(event.target.value);
 },100);
}
  1. React 会在派发事件时关上批量更新,此时所有的 setState 都会变成异步。
function onClick(event) {setState({a: 1}); // 1
 setState({a: 2}); // 2
 setTimeout(() => {setState({a: 3}); // 3
 setState({a: 4}); // 4
 },0);
}

此时 1,2 在事件内所以是异步的,二者只会触发一次 render 操作,3,4 是同步的,3,4 别离都会触发一次 render。

  1. React onClick/onClickCapture,实际上都产生在原生事件的冒泡阶段。
document.addEventListener('click',console.log.bind(null,'native'));
function onClickCapture() {console.log('capture');
}
<div onClickCapture={onClickCapture}/>

这里咱们尽管应用了onClickCapture,但实际上对原生事件而言仍然是冒泡,所以 React 16 中实际上就不反对绑定捕捉事件。

  1. 因为所有事件都注册到顶层事件上,所以多实个 ReactDOM.render 会存在抵触。

如果咱们渲染一个子树应用另一个版本的 React 实例创立,那么即便在子树中调用了 e.stopPropagatio 事件仍然会流传。所以多版本的 React 在事件上存在抵触。
最初咱们就能够轻松了解 React 事件零碎的架构图了

4. React 17 中事件零碎有哪些新个性

React 17 目前曾经公布了,官网称之为没有新个性的更新,对于使用者而言没有提供相似 Hooks 这样爆炸的个性,也没有 Fiber 这样的重大重构,而是积攒了大量 Bugfix,修复了之前存在的诸多缺点。其中变动最大的就数对事件零碎的革新了。
上面是笔者列举的一些事件相干的个性更新

调整将顶层事件绑在 container 上,ReactDOM.render(app,container);


将顶层事件绑定在 container 上而不是 document 上可能解决咱们遇到的多版本共存问题,对微前端计划是个重大利好。

对齐原生浏览器事件

React 17 中终于反对了原生捕捉事件的反对,对齐了浏览器原生规范。
同时onScroll 事件不再进行事件冒泡。
onFocusonBlur 应用原生 focusinfocusout 合成。

Aligning with Browsers
We’ve made a couple of smaller changes related to the event system:
The onScroll event no longer bubbles to prevent common confusion.
React onFocus and onBlur events have switched to using the native focusin and focusout events under the hood,which more closely match React’s existing behavior and sometimes provide extra information.
Capture phase events (e.g. onClickCapture) now use real browser capture phase listeners.

勾销事件复用


官网的解释是事件对象的复用在古代浏览器上性能曾经进步的不显著了,反而还很容易让人用错,所以罗唆就放弃这个优化。
参考

  1. https://reactjs.org/docs/even…
  2. https://reactjs.org/docs/hand…
  3. https://github.com/facebook/r…

本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0