react事件系统(老版本)

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react事件系统(老版本)相关的知识,希望对你有一定的参考价值。

带着问题阅读探索

  • 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进阶实践指南》

以上是关于react事件系统(老版本)的主要内容,如果未能解决你的问题,请参考以下文章

react事件系统(老版本)

react事件系统(新老版本)

react事件注册

react源码debugger-17,18版本的事件更新流程

react源码debugger-17,18版本的事件更新流程

前端每周清单: D3 5.0,深入 React 事件系统,SketchCode