本文做者: 江水html
React 为咱们提供了一套虚拟的事件系统,这套虚拟事件系统是如何工做的,笔者对源码作了一次梳理,整理了下面的文档供你们参考。前端
在 React事件介绍 中介绍了合成事件对象以及为何提供合成事件对象,主要缘由是由于 React 想实现一个全浏览器的框架, 为了实现这种目标就须要提供全浏览器一致性的事件系统,以此抹平不一样浏览器的差别。react
合成事件对象颇有意思,一开始听名字会以为很奇怪,看到英文名更奇怪 SyntheticEvent
, 实际上合成事件的意思就是使用原生事件合成一个 React 事件, 例如使用原生click
事件合成了onClick
事件,使用原生mouseout
事件合成了onMouseLeave
事件,原生事件和合成事件类型大部分都是一一对应,只有涉及到兼容性问题时咱们才须要使用不对应的事件合成。合成事件并非 React 的独创,在 iOS 上遇到的 300ms 问题而引入的 fastclick 就使用了 touch 事件合成了 click 事件,也算一种合成事件的应用。git
了解了 React 事件是合成事件以后咱们看待事件的角度就会有所不一样, 例如咱们常常在代码中写的这种代码github
<button onClick={handleClick}>
Activate Lasers
</button>
复制代码
咱们已经知道这个onClick
只是一个合成事件而不是原生事件, 那这段时间究竟发生了什么? 原生事件和合成事件是如何对应起来的?数组
上面的代码看起来很简洁,实际上 React 事件系统工做机制比起上面要复杂的多,脏活累活全都在底层处理了, 简直框架劳模。其工做原理大致上分为两个阶段浏览器
下面就一块儿来看下这两个阶段到底是如何工做的, 这里主要从源码层分析,并以 16.13 源码中内容为基准。markdown
React 既然提供了合成事件,就须要知道合成事件与原生事件是如何对应起来的,这个对应关系存放在 React 事件插件中EventPlugin
, 事件插件能够认为是 React 将不一样的合成事件处理函数封装成了一个模块,每一个模块只处理本身对应的合成事件,这样不一样类型的事件种类就能够在代码上解耦,例如针对onChange
事件有一个单独的LegacyChangeEventPlugin
插件来处理,针对onMouseEnter
, onMouseLeave
使用 LegacyEnterLeaveEventPlugin
插件来处理。架构
为了知道合成事件与原生事件的对应关系,React 在一开始就将事件插件所有加载进来, 这部分逻辑在 ReactDOMClientInjection 代码以下app
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'],
...
}
复制代码
这个对象便是一开始咱们说到的合成事件到原生事件的映射,对于onClick
和 onClickCapture
事件, 只依赖原生click
事件。可是对于 onMouseLeave
它倒是依赖了两个mouseout
, mouseover
, 这说明这个事件是 React 使用 mouseout
和 mouseover
模拟合成的。正是由于这种行为,使得 React 可以合成一些哪怕浏览器不支持的事件供咱们代码里使用。
第三个对象是 plugins, 这个对象就是上面注册的全部插件列表。
plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
复制代码
看完上面这些信息后咱们再反过头来看下一个普通的EventPlugin
长什么样子。一个 plugin 就是一个对象, 这个对象包含了下面两个属性
// event plugin
{
eventTypes, // 一个数组,包含了全部合成事件相关的信息,包括其对应的原生事件关系
extractEvents: // 一个函数,当原生事件触发时执行这个函数
}
复制代码
了解上面这这些信息对咱们分析 React 事件工做原理将会颇有帮助,下面开始进入事件绑定阶段。
document
上,回调为React提供的dispatchEvent函数。上面的阶段说明:
document
上。dispatchEvent
函数。onClick
, 最终反应在 DOM 事件上只会有一个listener
。listener
绑在原生事件上,也没有去维护一个相似eventlistenermap
的东西存放咱们的listener
。由 3,4 条规则能够得出,咱们业务逻辑的listener
和实际 DOM 事件压根就不要紧,React 只是会确保这个原生事件可以被它本身捕捉到,后续由 React 来派发咱们的事件回调,当咱们页面发生较大的切换时候,React 能够什么都不作,从而免去了去操做removeEventListener
或者同步eventlistenermap
的操做,因此其执行效率将会大大提升,至关于全局给咱们作了一次事件委托,即使是渲染大列表,也不用开发者关心事件绑定问题。
咱们知道因为全部类型种类的事件都是绑定为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
为事件执行时组件的层级关系存储,也就是若是在事件执行过程当中发生组件结构变动,并不会影响事件的触发流程。
整个触发事件流程以下:
dispatchEvent
函数。dispatchEvent
执行 batchedEventUpdates(handleTopLevel)
, batchedEventUpdates 会打开批量渲染开关并调用 handleTopLevel
。对于大部分事件而言其处理逻辑以下,也即 LegacySimpleEventPlugin
插件作的工做
SyntheticMouseEvent
) 。div
, a
这种原生组件)。onClickCapture
的实例。onClick
的实例。这几个阶段说明了下面的现象:
event.persist()
告诉 React 这个对象须要持久化。( React17 中被废弃)onClick
事件, 在执行这些onClick
以前 React 会打开批量渲染开关,这个开关会将全部的setState
变成异步函数。onClick
。document
上.这点很好理解,React的事件实际上都是在document
上触发的。
因此下面就是错误用法
function onClick(event) {
setTimeout(() => {
console.log(event.target.value);
}, 100);
}
复制代码
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。
onClick
/onClickCapture
, 实际上都发生在原生事件的冒泡阶段。document.addEventListener('click', console.log.bind(null, 'native'));
function onClickCapture() {
console.log('capture');
}
<div onClickCapture={onClickCapture}/>
复制代码
这里咱们虽然使用了onClickCapture
, 但实际上对原生事件而言依然是冒泡,因此 React 16 中实际上就不支持绑定捕获事件。
ReactDOM.render
会存在冲突。若是咱们渲染一个子树使用另外一个版本的 React 实例建立, 那么即便在子树中调用了 e.stopPropagatio
事件依然会传播。因此多版本的 React 在事件上存在冲突。
最后咱们就能够轻松理解 React 事件系统的架构图了
React 17 目前已经发布了, 官方称之为没有新特性的更新, 对于使用者而言没有提供相似 Hooks 这样爆炸的特性,也没有 Fiber 这样的重大重构,而是积攒了大量 Bugfix,修复了以前存在的诸多缺陷。其中变化最大的就数对事件系统的改造了。
下面是笔者列举的一些事件相关的特性更新
将顶层事件绑定在 container
上而不是 document
上可以解决咱们遇到的多版本共存问题,对微前端方案是个重大利好。
React 17 中终于支持了原生捕获事件的支持, 对齐了浏览器原生标准。
同时onScroll
事件再也不进行事件冒泡。
onFocus
和 onBlur
使用原生 focusin
, focusout
合成。
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.
官方的解释是事件对象的复用在现代浏览器上性能已经提升的不明显了,反而还很容易让人用错,因此干脆就放弃这个优化。
参考
本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!