React事件机制 您所在的位置:网站首页 addeventlistener监听点击事件与click事件有什么区别 React事件机制

React事件机制

2023-05-02 07:46| 来源: 网络整理| 查看: 265

对事件机制的初步理解和验证

对 react事件机制 的表象理解,验证,意义和思考。

一、原生事件回顾

在开始介绍react事件机制之前,我们先简单回顾 JavaScript 原生事件中几个重要知识点:

930650629-9c5b98ccdd063d03_fix732.png

1. 事件捕获

当某个元素触发某个事件(如 onclick ),顶层对象 document 就会发出一个事件流,随着 DOM 树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。在这个过程中,事件相应的监听函数是不会被触发的。

2. 事件目标

当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。

3. 事件冒泡

从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被触发一次。如果想阻止事件起泡,可以使用 e.stopPropagation() 或者 e.cancelBubble=true(IE)来阻止事件的冒泡传播。

4. 事件委托/事件代理

简单理解就是将一个响应事件委托到另一个元素。 当子节点被点击时,click 事件向上冒泡,父节点捕获到事件后,我们判断是否为所需的节点,然后进行处理。其优点在于减少内存消耗和动态绑定事件。

二、React事件机制初识

react 事件机制基本理解:react自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。

我们都知道react 的所有事件并没有绑定到具体的dom节点上而是绑定在了document 上,然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),所有节点的事件都会在 document 上触发。

1. 我们写在JSX事件终将变成什么?

我们先写一段含有点击事件的react JSX语法,看一下它最终会变成什么样子?

class Index extends React.Component{ handerClick= (value) => console.log(value) render(){ return 按钮点击 } } 复制代码

经过babel转换成React.createElement形式,如下:

babel.jpg

事件被存储在React.createElement的props中

最终转成fiber对象形式如下:

fiber.jpg

fiber对象上的memoizedProps 和 pendingProps保存了我们的事件。

2. 事件会绑定到哪里

接下来我们搞搞事情😂😂😂,在demo项目中加上一个input输入框,并绑定一个onChange事件。睁大眼睛看看接下来会发生什么?

class Index extends React.Component{ componentDidMount(){ console.log(this) } handerClick= (value) => console.log(value) handerChange=(value) => console.log(value) render(){ return 按钮点击 } } 复制代码

我们先看一下input dom元素上绑定的事件

22BEC470-233A-4C50-9C47-D21D343C055D.jpg

然后我们看一下document上绑定的事件

8E1D3BDB-ACFB-4E49-A5FF-CF990C47A60E.jpg

我们发现,我们给绑定的onChange,并没有直接绑定在input上,而是统一绑定在了document上,然后我们onChange被处理成很多事件监听器,比如blur , change , input , keydown , keyup 等。

综上我们可以得出结论:

①我们在 jsx 中绑定的事件(demo中的handerClick,handerChange),根本就没有注册到真实的dom上。是绑定在document上统一管理的。 ②真实的dom上的click事件被单独处理,已经被react底层替换成空函数。 ③我们在react绑定的事件,比如onChange,在document上,可能有多个事件与之对应。 ④ react并不是一开始,把所有的事件都绑定在document上,而是采取了一种按需绑定,比如发现了onClick事件,再去绑定document click事件。 三、React 事件与原生事件执行顺序

试想一下:

如果一个节点上同时绑定了合成和原生事件,那么禁止冒泡后执行关系是怎样的呢?

其实读到这里答案已经有了。我们现在基于目前的知识去分析下这个关系。

因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 dispatchEvent统一去处理。

得出的结论:

原生事件阻止冒泡肯定会阻止合成事件的触发。

合成事件的阻止冒泡不会影响原生事件。

为什么呢?先回忆下浏览器事件机制

浏览器事件的执行需要经过三个阶段,捕获阶段-目标元素阶段-冒泡阶段。

节点上的原生事件的执行是在目标阶段,然而合成事件的执行是在冒泡阶段,所以原生事件会先合成事件执行,然后再往父节点冒泡。

既然原生都阻止冒泡了,那合成还执行个啥嘞。

好,轮到合成的被阻止冒泡了,那原生会执行吗?当然会了。

因为原生的事件先于合成的执行,所以合成事件内阻止的只是合成的事件冒泡。

所以得出结论:

原生事件(阻止冒泡)会阻止合成事件的执行

合成事件(阻止冒泡)不会阻止原生事件的执行

两者最好不要混合使用,避免出现一些奇怪的问题

这里我们手写一个简单示例来观察 React 事件和原生事件的执行顺序:

在 React 中,“合成事件”会以事件委托的方式绑定在组件最上层,并在组件卸载(unmount)阶段自动销毁绑定的事件。

class App extends React.Component { parentRef: any; childRef: any; constructor(props: any) { super(props); this.parentRef = React.createRef(); this.childRef = React.createRef(); } componentDidMount() { console.log("React componentDidMount!"); this.parentRef.current?.addEventListener("click", () => { console.log("原生事件:父元素 DOM 事件监听!"); }); this.childRef.current?.addEventListener("click", () => { console.log("原生事件:子元素 DOM 事件监听!"); }); document.addEventListener("click", (e) => { console.log("原生事件:document DOM 事件监听!"); }); } parentClickFun = () => { console.log("React 事件:父元素事件监听!"); }; childClickFun = () => { console.log("React 事件:子元素事件监听!"); }; render() { return ( 分析事件执行顺序 ); } } export default App; 复制代码

触发事件后,可以看到控制台输出:

原生事件:子元素 DOM 事件监听! 原生事件:父元素 DOM 事件监听! React 事件:子元素事件监听! React 事件:父元素事件监听! 原生事件:document DOM 事件监听! 复制代码

通过上面流程,我们可以理解:

React 所有事件都挂载在 document 对象上; 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件; 所以会先执行原生事件,然后处理 React 事件; 最后真正执行 document 上挂载的事件。

1167376921-c6053295bf702260_fix732.png

1. e.stopPropagation

对于开发者来说,更希望使用 e.stopPropagation() 方法来阻止当前 DOM 事件冒泡,但事实上,从前两节介绍的执行顺序可知,e.stopPropagation() 只能阻止合成事件间冒泡,即下层的合成事件,不会冒泡到上层的合成事件。事件本身还都是在 document 上执行。所以最多只能阻止 document 事件不能再冒泡到 window 上。

class App extends React.Component { parentRef: any; childRef: any; constructor(props: any) { super(props); this.parentRef = React.createRef(); } componentDidMount() { this.parentRef.current?.addEventListener("click", () => { console.log("阻止原生事件冒泡~"); }); document.addEventListener("click", (e) => { console.log("原生事件:document DOM 事件监听!"); }); } parentClickFun = (e: any) => { e.stopPropagation(); console.log("阻止合成事件冒泡~"); }; render() { return ( 点击测试“合成事件和原生事件是否可以混用” ); } } export default App; 复制代码

输出结果:

阻止原生事件冒泡~ 阻止合成事件冒泡~ 复制代码 2. 合成事件和原生事件是否可以混用

合成事件和原生事件最好不要混用。 原生事件中如果执行了stopPropagation方法,则会导致其他React事件失效。因为所有元素的事件将无法冒泡到document上。 通过前面介绍的两者事件执行顺序来看,所有的 React 事件都将无法被注册。通过代码一起看看:

class App extends React.Component { parentRef: any; childRef: any; constructor(props: any) { super(props); this.parentRef = React.createRef(); } componentDidMount() { this.parentRef.current?.addEventListener("click", (e: any) => { e.stopPropagation(); console.log("阻止原生事件冒泡~"); }); document.addEventListener("click", (e) => { console.log("原生事件:document DOM 事件监听!"); }); } parentClickFun = (e: any) => { console.log("阻止合成事件冒泡~"); }; render() { return ( 点击测试“合成事件和原生事件是否可以混用” ); } } export default App; 复制代码

输出结果:

阻止原生事件冒泡~ 复制代码 四、意义

react 自己做这么多的意义是什么?

减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次 统一规范,解决 ie 事件兼容问题,简化事件逻辑 对开发者友好 对于合成的理解 一、概念介绍

React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。

即在react中,我们绑定的事件onClick等,并不是原生事件,而是由原生事件合成的React事件,比如 click事件合成为onClick事件。比如blur , change , input , keydown , keyup等 , 合成为onChange。

看个简单示例:

const button = Leo 按钮 复制代码

在 React 中,所有事件都是合成的,不是原生 DOM 事件,但可以通过 e.nativeEvent 属性获取 DOM 事件。

const handleClick = (e) => console.log(e.nativeEvent);; const button = Leo 按钮 复制代码

学习一个新知识的时候,一定要知道为什么会出现这个技术。 那么 React 为什么使用合成事件?其主要有三个目的:

进行浏览器兼容,实现更好的跨平台

React 采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React 提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。

避免垃圾回收

事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React 事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收) 。

方便事件统一管理和事务机制

本文不介绍源码啦,对具体实现的源码有兴趣的朋友可以查阅:《React SyntheticEvent》 。

二、合成事件与原生事件区别

React 事件与原生事件很相似,但不完全相同。这里列举几个常见区别:

1. 事件名称命名方式不同

原生事件命名为纯小写(onclick, onblur),而 React 事件命名采用小驼峰式(camelCase),如 onClick 等:

// 原生事件绑定方式 Leo 按钮命名 // React 合成事件绑定方式 const button = Leo 按钮命名 复制代码 2. 事件处理函数写法不同

原生事件中事件处理函数为字符串,在 React JSX 语法中,传入一个函数作为事件处理函数。

// 原生事件 事件处理函数写法 Leo 按钮命名 // React 合成事件 事件处理函数写法 const button = Leo 按钮命名 复制代码 3. 阻止默认行为方式不同

在原生事件中,可以通过返回 false 方式来阻止默认行为,但是在 React 中,需要显式使用 preventDefault() 方法来阻止。 这里以阻止  标签默认打开新页面为例,介绍两种事件区别:

// 原生事件阻止默认行为方式 Leo 阻止原生事件 // React 事件阻止默认行为方式 const handleClick = e => { e.preventDefault(); console.log('Leo 阻止原生事件~'); } const clickElement = Leo 阻止原生事件 复制代码 4. 小结

小结前面几点区别:

原生事件React 事件事件名称命名方式名称全部小写(onclick, onblur)名称采用小驼峰(onClick, onBlur)事件处理函数语法字符串函数阻止默认行为方式事件返回 false使用 e.preventDefault() 方法 三、合成事件的事件池 1. 事件池介绍

合成事件对象池,是 React 事件系统提供的一种性能优化方式。合成事件对象在事件池统一管理,不同类型的合成事件具有不同的事件池。

当事件池未满时,React 创建新的事件对象,派发给组件。 当事件池装满时,React 从事件池中复用事件对象,派发给组件。

关于“事件池是如何工作”的问题,可以看看下面图片:

713162671-7628281677c0e5e1_fix732.png

2. 事件池分析(React 16 版本)

React 事件池仅支持在 React 16 及更早版本中,在 React 17 已经不使用事件池。 下面以 React 16 版本为例:

function handleChange(e) { console.log("原始数据:", e.target) setTimeout(() => { console.log("定时任务 e.target:", e.target); // null console.log("定时任务:e:", e); }, 100); } function App() { return ( 测试事件池 ); } export default App; 复制代码

可以看到输出:

1423304014-7f63d1beedc4b53b_fix732.png

对于一次点击事件的处理函数,在正常的函数执行上下文中打印e.target就指向了dom元素,但是在setTimeout中打印却是null,如果这不是React事件系统,两次打印的应该是一样的,但是为什么两次打印不一样呢? 因为在React采取了一个事件池的概念,每次我们用的事件源对象,在事件函数执行之后,可以通过releaseTopLevelCallbackBookKeeping等方法将事件源对象释放到事件池中,这样的好处每次我们不必再创建事件源对象,可以从事件池中取出一个事件源对象进行复用,在事件处理函数执行完毕后,会释放事件源到事件池中,清空属性,这就是setTimeout中打印为什么是null的原因了。

在 React 16 及之前的版本,合成事件对象的事件处理函数全部被调用之后,所有属性都会被置为 null 。这时,如果我们需要在事件处理函数运行之后获取事件对象的属性,可以使用 React 提供的 e.persist() 方法,保留所有属性:

// 只修改 handleChange 方法,其他不变 function handleChange(e) { // 只增加 persist() 执行 e.persist(); console.log("原始数据:", e.target) setTimeout(() => { console.log("定时任务 e.target:", e.target); // null console.log("定时任务:e:", e); }, 100); } 复制代码

再看下结果:

2980780761-7a15602d5352ad5b_fix732 (1).png

3. 事件池分析(React 17 版本)

React v17 整体改动不是很大,但是事件系统的改动却不小,首先上述的很多执行函数,在v17版本不复存在了。我来简单描述一下v17事件系统的改版。

1 事件统一绑定container上,ReactDOM.render(app, container);而不是document上,这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在document上,那么可能多应用下会出现问题。

react_17_delegation.png

2 对齐原生浏览器事件

React 17 中终于支持了原生捕获事件的支持, 对齐了浏览器原生标准。同时 onScroll 事件不再进行事件冒泡。onFocus 和 onBlur 使用原生 focusin, focusout 合成。

3 取消事件池 React 17 取消事件池复用,也就解决了上述在setTimeout打印,找不到e.target的问题。

四、常见问题 1. React 事件中 this 指向问题

在 React 中,JSX 回调函数中的 this 经常会出问题,在 Class 中方法不会默认绑定 this,就会出现下面情况, this.funName 值为 undefined :

class App extends React.Component { childClickFun = () => { console.log("React 事件"); }; clickFun() { console.log("React this 指向问题", this.childClickFun); // undefined } render() { return ( React this 指向问题 ); } } export default App; 复制代码

我们有 2 种方式解决这个问题:

使用 bind 方法绑定 this : class App extends React.Component { constructor(props: any) { super(props); this.clickFun = this.clickFun.bind(this); } // 省略其他代码 } export default App; 复制代码 将需要使用 this 的方法改写为使用箭头函数定义: class App extends React.Component { clickFun = () => { console.log("React this 指向问题", this.childClickFun); // undefined } // 省略其他代码 } export default App; 复制代码

或者在回调函数中使用箭头函数:

class App extends React.Component { // 省略其他代码 clickFun() { console.log("React this 指向问题", this.childClickFun); // undefined } render() { return ( this.clickFun()}>React this 指向问题 ); } } export default App; 复制代码 2. 向事件传递参数问题

经常在遍历列表时,需要向事件传递额外参数,如 id 等,来指定需要操作的数据,在 React 中,可以使用 2 种方式向事件传参:

const List = [1,2,3,4]; class App extends React.Component { // 省略其他代码 clickFun (id) {console.log('当前点击:', id)} render() { return ( 第一种:通过 bind 绑定 this 传参 { List.map(item => 按钮:{item}) } 第二种:通过箭头函数绑定 this 传参 { List.map(item => this.clickFun(item)}>按钮:{item}) } ); } } export default App; 复制代码

这两种方式是等价的:

第一种通过 Function.prototype.bind 实现; 第二种通过箭头函数实现。

参考文章:

segmentfault.com/a/119000003…

toutiao.io/posts/28of1…

juejin.cn/post/695563…



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有