读完本篇文章你将明白为什么是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----'); } useEffect(() => { document.addEventListener('click',()=>{ console.log('我到底有没有被阻止') }) }, []) return ( <div className="App" onClick={handleClick} onClickCapture={handleCapture}> <header className="App-header"> <h1>{count}</h1> <div onClick={(e) => { // e.stopPropagation(); setCount(count + 1); }}> 点击我 </div> </header> </div> );} 复制代码
- 执行dispatchEvent
- 创建事件对应的合成事件 SyntheticEvent
- 收集捕获的回调函数和对应的节点实例
- 收集冒泡的回调函数和对应的节点实例
- 执行对应的回调函数,同时将SyntheticEvent 作为参数传入
ok 我先给你分析一下SyntheticEvent 这个源码 其实很简单, 就是做了一件事, 合成事件, 做了兼容处理。
function createSyntheticEvent(Interface: EventInterfaceType) { function SyntheticBaseEvent( reactName: string | null, reactEventType: string, targetInst: Fiber, nativeEvent: {[propName: string]: mixed}, nativeEventTarget: null | EventTarget, ) { this._reactName = reactName;// 事件名字 this._targetInst = targetInst; // Fiber 节点 this.type = reactEventType; this.nativeEvent = nativeEvent; // 原生事件 this.target = nativeEventTarget; this.currentTarget = null; // 初始化 做一些赋值操作 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]; } } // 兼容可恶的ie const defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false; if (defaultPrevented) { this.isDefaultPrevented = functionThatReturnsTrue; } else { this.isDefaultPrevented = functionThatReturnsFalse; } // 默认是不冒泡的, 但是react如果调用了阻止冒泡会把这个函数设为functionThatReturnsTrue this.isPropagationStopped = functionThatReturnsFalse; return this; } Object.assign(SyntheticBaseEvent.prototype, { preventDefault: function() { // 掉用的就是原生的浏览器行为 this.defaultPrevented = true; 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() { // 原生的浏览器事件 React 17 const event = this.nativeEvent; if (!event) { return; } if (event.stopPropagation) { event.stopPropagation(); } else if (typeof event.cancelBubble !== 'unknown') { event.cancelBubble = true; } // 这就是React 自己在冒泡的时候 函数终止的条件 this.isPropagationStopped = functionThatReturnsTrue; }, // 不在使用事件池了 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 大家感兴趣地话,可以自行阅读源码。