读完本篇文章你将明白为什么是React的合成事件SyntheticEvent, 以及React如何模拟浏览器的捕获和冒泡。
在学习React的合成事件之前,我们先复习下浏览器的事件系统,以及代理委托。这对我理解React事件系统源码非常重要。
W3C 标准约定了一个事件的传播过程要经过以下 3 个阶段:
- 事件捕获阶段
- 目标阶段
- 冒泡阶段
理解这个过程最好的方式就是读图了,下图是一棵 DOM 树的结构简图,图中的箭头就代表着事件的“穿梭”路径。
当我们点击了一个事件, 首先做的的第一件事就是从外层的元素,直接穿梭到我们的目标元素。这个阶段会执行所有捕获阶段的函数, ok, 然后事件流切换到目标阶段,执行自身的事件函数,这时候事件流在沿着相反的方向一直向上执行所有函数。 OK 我们在dom节点 绑定过多的监听事件,必定造成内存浪费。 所以就有一种优化: 就是事件委托
像这样利用事件的冒泡特性,把多个子元素的同一类型的监听逻辑,合并到父元素上通过一个监听函数来管理的行为,就是事件委托。
合成事件——SyntheticEvent
为什么React要自己实现一套事件系统?
答: React 作为一个框架, ok框架的必须要想的多, ok自然想到了可恶的IE, 和通用的浏览器根本不太一样。
从源码中随便举个简单的小例子:
if (event.preventDefault) { event.preventDefault(); // $FlowFixMe - flow is not aware of `unknown` in IE } else if (typeof event.returnValue !== 'unknown') { event.returnValue = false; }
复制代码
第二个很重要的 自研事件系统使 React 牢牢把握住了事件处理的主动权, 举个大家都比较熟悉的例子,比如说它想在事件系统中处理 Fiber 相关的优先级概念,或者想把多个事件揉成一个事件(比如 onChange 事件) 。
谈到事件,必定有绑定和触发
React事件的绑定:
React17中的事件的绑定其实已经绑定在diy#container上不在之前的document上了, 先给大家看一下整个流程的调用图:
这个过程是发生在初始化过程,此时dom节点还没有挂载。
listenToNativeEvent 这个函数做的事是非常的简单, 就是调用下面两个函数, addTrappedEventListener 和 addEventBubbleListener ok 我们着重分析下这两个函数,
let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, );
复制代码
第一件事就是创建监听函数, 这里React 对我们用的 事件做了优先级分类主要是以下3种
-
DiscreteEvent 例如: foucus | blur | click …
-
UserBlockingEvent 例如: mouseMove | mouseOver …
-
ContinuousEvent 例如: progress | load | error
不同优先级对应的对应的listenr不同,listener 和我们常见的事件监听的职责不太一样, 在React中listener 是一个统一地事件分发函数.
事件类型的有3种,同样listener 也有3种分发事件
-
dispatchDiscreteEvent
-
dispatchUserBlockingUpdate
-
dispatchEvent
第一种 和第二种的区别主要体现在优先级上,对事件分发动作倒没什么影响。无论是 dispatchDiscreteEvent 还是 dispatchUserBlockingUpdate,它们最后都是通过调用 dispatchEvent 来执行事件分发的。因此可以认为,最后绑定到div#root 上的这个统一的事件分发函数,其实就是 dispatchEvent。
React 如何dispacthEvent的 ?
我还是举一个例子说明 :
function App() { const [count, setCount] = useState(0); const handleClick = () => { console.error('App -----'); } const handleCapture = () => { console.error('cauture----'); } return ( <div className="App" onClick={handleClick} onClickCapture={handleCapture}> <header className="App-header"> <h1>{count}</h1> <div onClick={() => { setCount(count + 1); }}> 点击我 </div> </header> </div> );}
复制代码
当我点击了div 发生了什么事情
- 首先,点击的div 会冒泡到最上层的div 也就是 div #root
- 执行dispatchEvent
- 创建事件对应的合成事件 SyntheticEvent
- 收集捕获的回调函数和对应的节点实例
- 收集冒泡的回调函数和对应的节点实例
- 执行对应的回调函数,同时将SyntheticEvent 作为参数传入
ok 我先给你分析一下SyntheticEvent 这个源码 其实很简单, 就是做了一件事, 合成事件, 做了兼容处理。
function createSyntheticEvent(Interface: EventInterfaceType) { function SyntheticBaseEvent( reactName: string | null, // 接受的事件名字 reactEventType: string, // 事件的类型 targetInst: Fiber, // 对应的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; // InterFace 就是当前事件有哪些 参数 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;
// 默认行为是合成事件的preventDefault, 也是直接调用的原生事件的默认行为 const event = this.nativeEvent; if (!event) { return; } if (event.preventDefault) { event.preventDefault(); } 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(); } else if (typeof event.cancelBubble !== 'unknown') { event.cancelBubble = true; } this.isPropagationStopped = functionThatReturnsTrue; }, // react 17.0 已经取消事件池了 persist: function() { // Modern event system doesn't use pooling. }, isPersistent: functionThatReturnsTrue, }); return SyntheticBaseEvent;}
复制代码
敲重点:
- React 17 合成事件的 阻止冒泡 和阻止默认行为, 其实调用的就是绑定在当前节点的原生浏览器事件
1. 没有阻止冒泡的
2. 阻止冒泡了,按道理就不会冒泡到doucument上, 之前大家的阻止冒泡, 都是获取当前节点的ref,然后去阻止冒泡。 现在已经不用了。舒服
看图,document 上的事件已经被阻止了。
OK合成事件看完了,我们继续往下看, dispatchEvent 之后,中间某些复杂的操作
会收集当前所有捕获的合成event 和listener、 然后放到一个dispacthQueue 队列中,我们来看核心代码:
function processDispatchQueueItemsInOrder( event: ReactSyntheticEvent, dispatchListeners: Array<DispatchListener>, inCapturePhase: boolean,): void { let previousInstance;
// inCapturePhase 表示是否在捕获阶段。 if (inCapturePhase) { for (let i = dispatchListeners.length - 1; i >= 0; i--) { const {instance, currentTarget, listener} = dispatchListeners[i];
// 上文合成事件, 如果我们在合成事件调用了e.stopProgation(), 会把合成事件上
// this.isPropagationStopped = functionThatReturnsTrue; 这是个返回true 的函数
// 调用了阻止冒泡 函数调用就直接结束。 if (instance !== previousInstance && event.isPropagationStopped()) { return; } 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; } executeDispatch(event, listener, currentTarget); previousInstance = instance; } }}
复制代码
这里不知道你有没有注意到 一个正序遍历, 一个倒序遍历, 主要是React 在收集listeners, 是从我们点击的元素一层一层往上找, 这个过程其实其实和浏览器的冒泡阶段地行为是相符合的。 那么捕获极端自然就倒叙遍历, React 这个是真的非常秒哇。
还有一个特别重要的我要和大家强调的是,如果我在全局绑定了很多onClick 事件, 由于是事件代理到div#root ,所以呢合成事件只会创建一次,只是有很多dispacthListeners 而已, 而每个listener 包含了当前的事件的currentTarget。 直接上截图:
大家可以结合我这个数据,去分析上面的代码自然就清楚了。
最后 每一个listener调用后会将event.currentTarget = null;
好了,到这里React 事件系统 源码分析结束, 其实 React 针对不同的时间 有不同的事件插件Plugin, 然后合成事件也是在每个方法去实现。extraEvent 大家感兴趣地话,可以自行阅读源码。