带着问题阅读探索
- React 为什么有自己的事件系统?
- 什么是事件合成 ?
- 如何实现的批量更新?
- 事件系统如何模拟冒泡和捕获阶段?
- 如何通过 dom 元素找到与之匹配的fiber?
- 为什么不能用 return false 来阻止事件的默认行为?
- 事件是绑定在真实的dom上吗?如何不是绑定在哪里?
- V17 对事件系统有哪些改变?
React为什么要有自己的事件系统。
- 首先,对于不同的浏览器,对事件存在不同的兼容性,React 想实现一个兼容全浏览器的框架, 为了实现这个目标就需要创建一个兼容全浏览器的事件系统,以此抹平不同浏览器的差异。
- 其次,react将事件统一绑定到根容器上(17之前是document),防止过多事件直接绑定到原生dom上,这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件,也有利于一个 html 下存在多个应用(微前端)
- 竟然是统一绑定的事件,那么react就需要自己实现一套事件流,事件俘获-》事件源=〉事件冒泡,也包括事件对象event的重写。
什么是事件合成
- 上面已经知道react注册事件的时候会将所有事件注册到document/根容器上。
- 对于click,会绑定到document上。
- 而对于input的onChange事件,绑定到document上面的就多了blur,change ,focus ,keydown,keyup 等事件。
- react绑定事件并不是一次性绑定所有事件,比如发现了onClick,就绑定click事件,发现onChange,就绑定[blur,change ,focus ,keydown,keyup] 多个事件。
- 事件合成的概念就是:React应用中,事件并不是原生的事件,而是由react合成的事件,比如onCLick由click合成,而onChange由blur,change ,focus ,keydown,keyup 等事件合成。
事件系统
一共分为三部分:
- 事件合成系统,初始化注册不同的事件插件。
- 在一次渲染过程中,对事件标签中事件的收集,往container注册事件。
- 一次用户交互,事件触发,到时间执行一系列过程。
事件插件机制
react对于不同的事件,比如onClick和onChange,会有不同的事件插件处理。
const registrationNameModules = {
onBlur: SimpleEventPlugin,
onClick: SimpleEventPlugin,
onClickCapture: SimpleEventPlugin,
onChange: ChangeEventPlugin,
onChangeCapture: ChangeEventPlugin,
onMouseEnter: EnterLeaveEventPlugin,
onMouseLeave: EnterLeaveEventPlugin,
...
}
registrationNameModules存放着每个事件与之对应的处理插件的映射。比如onClick,就是SimpleEventPlugin。对于onChange,会使用ChangeEventPlugin处理…
看看SimpleEventPlugin和ChangeEventPlugin的真面目。
SimpleEventPlugin是一个对象,registerSimpleEvents顾名思义就是用来注册simple的事件的。
可以看到,遍历simpleEventPluginEvents数组,如keyUp,mouseDown都属于simple的事件。然后对每个事件,调用
register('keyUp', 'onKeyUp')
第一个参数就是原生事件,第二个参数就是react的事件。
然后调用registerTwoPhaseEvent( 'onKeyUp', ['keyUp'])
,
这就是simpleEventPlugin的registerEvents做的事情。
而onChange对应的ChangeEventPlugin
注册onChange,而onChange依赖的事件是change,click,…等诸多个原生事件。
而每个插件的extractEvents顾名思义就是提取事件源对象event。对于不同的事件,事件源对象不同。
而这也解释了为什么要用不同的插件对象处理事件?
对于不同的合成事件,有不同的处理逻辑;对应的事件源对象也不同,react事件和事件源是自己合成的。
而还有一个对象registrationNameDependencies,
就是上面说到每个插件调用registerEvent的时候,最终调用的是registerDirectEvent,而他会将事件的依赖收集到registrationNameDependecies上面,比如上面的onKeyup: [keyUp],而onChange就是对应: ['blur,‘change’…]
最终这个对象就长得像
{
onBlur: ['blur'],
onClick: ['click'],
onClickCapture: ['click'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
onMouseEnter: ['mouseout', 'mouseover'],
onMouseLeave: ['mouseout', 'mouseover'],
...
}
这个对象保存了React事件和原生事件的对应关系,发现有react事件的时候,就会通过这个对象找到原生事件数组,然后逐一绑定。
事件绑定
react在处理props的啥时候,如果遇到了事件比如onCLick,就会通过addEventListener注册原生事件。
然后我们绑定的事件比如
const Test = () => {
return <div onClick={handleClick}>123</div>
}
handleClick事件,他会存在div fiber上面的memoizedProps上面如
testFiber.child = divFiber
divFiber.child = testFiber
divFiber.memoizedProps = {onClick: handleClick}
接着需要通过react事件注册原生的事件。
react在处理props的时候,如果发现是合成事件的话,比如onClick,就会调用legacyListenToEvent 函数,
function diffProperties(){
/* 判断当前的 propKey 是不是 React合成事件 */
if(registrationNameModules.hasOwnProperty(propKey)){
/* 这里多个函数简化了,如果是合成事件, 传入成事件名称 onClick ,向document注册事件 */
legacyListenToEvent(registrationName, document);
}
}
上面介绍了registrationNameDependencies是存取react事件跟原生事件的绑定。如onChange对应[‘blur’, ‘change’, ‘click’, ‘focus’, ‘input’, ‘keydown’, ‘keyup’, ‘selectionchange’],通过react事件获取原生事件,通过for循环进行绑定。
其次,真正绑定到container上面的函数,并不是我们直接写的handleClcik函数,而是React统一的事件处理还能输,dispatchevent,只要是react事件触发,首先执行的就是dispatchEvent
给容器注册事件的时候,通过.bind传入了一些参数,这样dispatchEvent才能知道是什么事件触发。
最后则是事件触发
老的事件版本,使用的批量更新依然是通过开关开启或者关闭的。
第一步
比如点击一次之后,
dispatchEvent触发,传入真实的事件源button元素本身。
然后通过button dom可以找到对应的fiber
因为dom跟fiber之间的关系是
fiber.stateNode = dom
dom.xxxKey = fiber
伪代码类似于
export function batchedEventUpdates(fn,a){
isBatchingEventUpdates = true; //打开批量更新开关
try{
fn(a) // 事件在这里执行
}finally{
isBatchingEventUpdates = false //关闭批量更新开关
}
}
第二步 合成事件源
我们知道react事件的事件源也是react自己合成的,并不是原生的事件源。
onClick是由SimpleEventPlugin处理的,
对于onCLick,他的事件源就是SyntheticMouseEvent。所以第二阶段的模型就是:
(图片来自掘金大佬的课程 https://juejin.cn/book/6945998773818490884/section/6959723748450631694)
第三步,形成事件执行队列。
在第一步通过dom获取的fiber,从这个ifber往上遍历,遇到是元素类型的,比如div,p这些,就会通过一个数组来收集事件。
- 对于俘获事件如onClickCapture,就会unshift放在数组前面,以此模拟事件俘获阶段。
- 如果遇到冒泡事件onClick,就直接push在后面,模拟事件冒泡阶段。
- 一直收集到container,形成一个队列,在接下来的阶段,依次执行队列里的函数。
事件手机的逻辑,首先通过fiber.stateNode获取dom,然后通过getListener获取对应的注册事件。
通过fiber.stateNode获取上面的props,然后获取对应注册的注册事件。
接着如果是capture的,就unshift进数组,否则就push进数组,while循环直到收集结束。
那么对于以下demo
export default function Test(){
const handleClick1 = () => console.log(1)
const handleClick2 = () => console.log(2)
const handleClick3 = () => console.log(3)
const handleClick4 = () => console.log(4)
return <div onClick={ handleClick3 } onClickCapture={ handleClick4 } >
<button onClick={ handleClick1 } onClickCapture={ handleClick2 } >点击</button>
</div>
}
点击button
数组的顺序应该是: [ handleClick4, handleClick2, handleClick1, handleClick3 ]。
React如何阻止事件冒泡呢?
先看看事件如何执行的,react提供了event.isPropagationStopped来判断是否已经阻止事件冒泡。
数组通过for循环依次执行,而当调用event.stopPropagation之后,后面一个事件的执行event.isPropagationStopped就是true了,就会退出for循环,后面的函数不再执行,以此模拟阻止事件冒泡。
问答
React 为什么有自己的事件系统? 什么是事件合成?
- react有自己的一套事件系统,第一是为了兼容所有的浏览器,因为各浏览器之间的事件源可能不同。其次,react将所有事件注册到根容器上,,在挂载和卸载的时候可以方便统一处理,其次在17之前,react将所有事件注册到document上面,而在17之后,react将所有事件注册到根容器上,这样可以方便多个应用如微前端的接入。
- 其次,对于onClick,onChange等react事件,他们并不是原生的事件,比如onChange是由blur, keyup,focus,click等事件合成的,而onClick是由clikc事件合成的。对于不同的事件,react使用不同的插件进行注册,比如onCLick事件是用SimpleEventPlugin处理的,而onChange是由changeEventPlugin处理的。
如何模拟事件捕获和事件冒泡阶段? 为什么不能用 return false 来阻止事件的默认行为?
- react通过一个事件队列来收集事件,从发生事件开始,react通过原生dom获取对应的fiber,然后向上遍历元素类型的fiber,对于同一事件冒泡的处理函数,就直接push进数组,对于同一事件俘获的处理函数,就通过unshift插入数组,以此达到模拟事件俘获和事件冒泡的阶段
- 而阻止事件冒泡react提供了event.stopPropagiton函数,react执行事件队列是通过for循环的,event.isStopPropagation可以判断是否执行了event.stopPropagiton,一旦判断执行了之后就break退出for循环,那么事件队列后面的事件不会执行,以此达到阻止冒泡的效果。
一次点击到事件执行都发生了什么?
- 1 一次点击之后,首先执行dispatchEvent函数,传入对应的dom,通过dom获取fiber
- 2 然后通过registionNameMOdule对象,获取对应的插件,比如onCLick对应的是SimpleEventPlugin,然后通过插件获取onClick事件源,不同的事件事件源不同。
- 3 从当前fiber往上遍历元素类型的fiber,维护一个数组收集事件,对于俘获事件直接unshift,对于冒泡事件直接push,最终得到一个事件队列。
- 4 for循环执行事件队列,判断event.isStopPropagation,如果true表示阻止冒泡,直接break退出for循环,后面的事件不再执行。
17版本之前的事件,虽说模拟了事件冒泡和俘获,但是本质上,执行的时机,都是在冒泡阶段。而新版本的事件处理,修正了这个问题。让我们看看新版本的事件系统。
新版本的事件系统。
未完待续
- 参考掘金的 《react进阶实践指南》