- A+
事件系统
react v17
事件绑定
事件绑定在函数 setInitialDOMProperties
setInitialDOMProperties 将在 complete 阶段执行
function setInitialDOMProperties( tag: string, domElement: Element, rootContainerElement: Element | Document, nextProps: Object, isCustomComponentTag: boolean ): void { // *遍历 props for (const propKey in nextProps) { if (!nextProps.hasOwnProperty(propKey)) { continue; } const nextProp = nextProps[propKey]; if (...) { ... } // *registrationNameDependencies 包含 react 支持的所有的事件,如果当前的 propKey 是 react支持的事件就进入该 if else if (registrationNameDependencies.hasOwnProperty(propKey)) { if (nextProp != null) { // !注意,这里与 react v16 有所不同,v16 这里直接执行 ensureListeningTo 函数,但是 v17 这里不会执行。因为 enableEagerRootListeners 是一个常量,值一直为 true,if (false) 自然不会执行,并且在 react-dom.development.js 中直接没有这个 if,只剩下 onScroll 的判断。这样更能说明问题了 if (!enableEagerRootListeners) { // *忽略这个函数,并没有执行 ensureListeningTo(rootContainerElement, propKey, domElement); } else if (propKey === 'onScroll') { listenToNonDelegatedEvent('scroll', domElement); } } } else if (nextProp != null) { setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag); } } }
那么在 react v17 中遍历到 onClick 这种事件的时候貌似并没有做什么。那么事件绑定是什么时候绑定的呢?其实在最开始的 createRootImpl
也就是创建 HostRootFiber
时就通过 listenToAllSupportedEvents
将所有支持的事件都绑定到了 rootContainerElement
(这里也对应了 react v17 就将事件统统绑定到 rootContainer 而不是 document)
那么事件处理函数又是多久绑定的呢?
通过对绑定的事件处理函数进行 debugger 可以发现,其实根本没有将事件处理函数直接绑定到 rootContainerElement 上,而是直接使用的上面 listenToAllSupportedEvents
中绑定的事件。大概的流程为:
listenToAllSupportedEvents
为rootContainerElement
绑定所有的事件- 点击子组件,其实就相当于在点击
rootContainerElement
所以会触发对应的点击事件。 - 绑定事件的时候会根据事件优先级绑定不同的处理函数,但是最终其实都是执行 dispatchEvent
dispatchEvent
内部将将进入其他函数,获取触发事件的元素,然后根据对应的 Fiber 然后在根据很多层函数,最终执行事件处理函数。
事件触发
使用的案例
import React from 'react' import './index.css' class EventDemo extends React.Component{ state = { count: 0, } onDemoClick = e => {this.setState({ count: this.state.count + 1 })} onParentClick = () => {console.log('父级元素的点击事件被触发了');} onParentClickCapture = () => {console.log('父级元素捕获到点击事件');} onSubCounterClick = () => {console.log('子元素点击事件');} onSubCounterClickCapture = () => {console.log('子元素点击事件 capture')} render() { const { count } = this.state return <div className={'counter-parent'} onClick={this.onParentClick} onClickCapture={this.onParentClickCapture}> counter-parent <div onClick={this.onDemoClick} className={'counter'}> counter:{count} <div className={'sub-counter'} onClick={this.onSubCounterClick} onClickCapture={this.onSubCounterClickCapture}> 子组件 </div> </div> </div> } } export default EventDemo
- 点击子元素后,自然会执行 dispatchEvent
- 然后会进入 attemptToDispatchEvent
(如果没有正在进行的事件?因为在进入 attemptToDispatchEvent 之前会进行 hasQueuedDiscreteEvents
hasQueuedDiscreteEvents 判断 具体可以看 dispatchEvent)
然后在attemptToDispatchEvent
中会通过原生的事件参数(event)获取到触发事件的 DOM,然后通过该 DOM 获取到对应的 Fiber
然后正常情况下会进入 dispatchEventForPluginEventSystem. dispatchEventForPluginEventSystem
一般会进入批量更新,也就是batchEventUpdates
,与 render 时的一样,也会传入一个匿名函数,不过该匿名函数内部执行的是:dispatchEventsForPlugins
.dispatchEventsForPlugins
内部又执行extractEvents
函数extractEvents
函数内部又会使用EventPlugin
创建 react合成事件 的 Event 参数,并且会遍历 Fiber 链表,将将会触发的事件统统放到 dispatchQueue 中(具体遍历 Fiber 的函数是在 accumulateSinglePhaseListeners )。
accumulateSinglePhaseListeners
具体流程如下- 首先会判断当前是 捕获阶段 还是 冒泡阶段 根据阶段的不同,使用不同的 reactEventName (例如:onClick 还是 onClickCapture)
- 然后会进入 while 循环,循环中会通过
reactEventName
获取instance
的事件处理函数,即listener
如果listener
不为 null 那么就会将{ currentTarget, instance, listener }
放到listeners
中(currentTarget
是当前的 dom 元素,instance
是当前的 Fiber,listener
是当前的事件处理函数),接着将instance
指向instance.return
继续 while 循环。 - while 循环结束后,会将
listeners
返回出去。
extractEvents
执行完成后,就会开始执行 dispatchQueue 中的内容了。
针对我们的案例,分析一下具体的流程
- 点击子元素,那么就会直接触发 root 的 clickCapture 事件
- 进入 dispatchEvent
- 进入 attemptToDispatchEvent, 获取真实触发事件的 dom 和对应的 Fiber
- 进入 dispatchEventForPluginEventSystem
- 进入 batchEventUpdates
- 进入 dispatchEventsForPlugins
- 进入 extractEvent 获取 react合成事件参数
- 进入 accumulateSinglePhaseListeners 获取 listeners 数组因为当前是捕获阶段(在代码中会判断是什么阶段),所以就只会收集捕获阶段的事件处理函数(直接 push 到 listeners 并不会像小册中说的那样 遇到捕获事件就 unshift 可能是版本问题),经过测试可以得知,无论案例中是否绑定了 clickCapture 都会去试图收集捕获阶段的事件处理函数, 只是收集不到而已
- 返回 extractEvent 将 listeners 放到 dispatchQueue 中去
- 返回 dispatchEventsForPlugins 进入 processDispatchQueue 内部会判断当前到底是什么阶段,接着循环 dispatchQueue
- 进入 processDispatchQueueItemsInOrder ,根据阶段不同,按照不同的顺序执行 listeners,比如捕获阶段的话,就是从后往前,冒泡阶段的话就是从前往后。
- 再经过一系列函数的包裹,最终顺利执行函数。
- capture 阶段完成后,直接进入 bubble 阶段,再次按照上面的顺序执行,最终 bubble 阶段也完成。就是这样。
注意:listeners 的结构应该是 { currentTarget, instance, listener }, dispatchQueue 的结构应该是 [ { event, listeners } ] 此时的 event 应该是 React合成事件 event
注意:在此案例中,无论是捕获阶段,还是冒泡阶段,因为 listeners 是一个数组(该阶段将要触发的所有 listener 数组),所以 dispatchQueue 中都只有一个元素,不清楚在上面情况下 dispatchQueue 才有多个元素。