
【跟着ChatGPT学React】事件
事件
首先 react 里面的事件都是 react 自己实现的,和 js 原生的事件是不一样的。是合成事件(Synthetic Event)。
1 原生 js 事件
首先要知道 JS 的原生事件。原生 js 是怎么触发事件的呢?
下面写了三种触发事件的方式。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Document</title> </head> <body> <button id="btn1">按钮1</button> <button id="btn2">按钮2</button> <button onclick="demo()">按钮3</button> <script type="text/javascript"> // 方法1 addEventListener() const btn1 = document.getElementById('btn1'); btn1.addEventListener('click', () => { alert('按钮1被点击了'); }); // 方法2 onclick const btn2 = document.getElementById('btn2'); btn2.onclick = () => { alert('按钮2被点击了'); }; // 方法3 直接写在html属性里面 onclick="demo()" function demo() { alert('按钮3被点击了'); } </script> </body> </html>
其实原生的 JS 事件还有其他可以写的。比如事件和事件监听,事件冒泡和捕获等等。
对比示例代码
import React, { useEffect } from 'react'; function App() { useEffect(() => { document.getElementById('btn').addEventListener('click', (event) => { console.log('原生事件:', event); }); }, []); const handleClick = (event) => { console.log('合成事件:', event); // React 的 SyntheticEvent }; return ( <button id="btn" onClick={handleClick}> 点击我 </button> ); } export default App; // 合成事件: SyntheticEvent { ... } // 原生事件: PointerEvent { ... }
特性 | 合成事件(SyntheticEvent) | 原生事件(Native Event) |
---|---|---|
由谁提供 | React | 浏览器 |
兼容性 | 统一封装,跨浏览器兼容 | 需要手动处理不同浏览器的差异 |
事件池 | 使用事件池,事件对象会被回收 | 事件对象不会被回收 |
事件绑定 | 事件委托到 document | 直接绑定到 DOM 元素 |
2 React 的事件
首先 React 基本上是原生时间首字母大写!
onClick()
合成事件onclick()
原生事件
为什么需要合成事件?
在原生 JavaScript 事件处理中,我们通常使用 addEventListener 监听事件,但不同浏览器对事件的实现存在差异。React 为了解决这个问题,引入了 **合成事件(SyntheticEvent)
- 提供跨浏览器兼容性,避免手动处理不同浏览器的事件差异。
- 事件对象会被自动回收,减少内存消耗,提高性能。
- React 使用事件代理(Event Delegation),在 document 级别监听所有事件,提高性能。
因为 react 为了不仅仅适配 PC 端浏览器的 DOM 事件,还有手机 iOS,Android 等等原生事件。为了让不同的平台都可以用同一个代码,所以 react 决定用合成事件。
⚠️ 以下全部是为了解决类组件的痛点,如何触发一个事件,调用你写的函数?
首先来复习下,类的用法吧。。
class Animal {} class Dog extends Animal { // 不能用funcion定义方法 // ❌ function wang(){} wang() { console.log('wang'); } eat() { const wangwang = () => { console.log('wangwang'); }; // 在eat外面定义的函数必须要用this,自己内部定义的函数才能直接call // ❌ wang(); this.wang(); wangwang(); } } const d = new Dog(); d.wang(); d.eat();
错误写法
然后接下来在看一下下面的调用函数
错误写法 1️⃣
display() { console.log("display"); } render() { return ( <div> hello home <button onClick="display()">click me!</button> {/* ❌这样直接写只是个字符串!!❌ 而且还没用this*/} </div> ); } // Expected `onClick` listener to be a function, instead got a value of `string` type.
错误写法 2️⃣
看到{}
可以直接理解成渲染的时候就立即调用。所以这里面只能放函数体。而不能是函数调用
<h1 onClick={要放的是函数体}><h1/>
<h1 onClick={()=>{}}><h1/>
→ 直接在里面写匿名函数可以!
<h1 onClick={demo}><h1/>
→ 外面写函数也可以!
display() { console.log("display"); } render() { return ( <div> hello home <button onClick={this.display()}>click me!</button> {/* ❌这里会立即执行这个函数*/} </div> ); } // 因为这属于赋值,相当于把demo()给调用,并且把undefined返回值给了onClick
✅ 写法
class Home extends Component { display = () => { console.log('display'); }; render() { return ( <div> {/* 相当于这里直接放了函数体 */} <button onClick={this.display}>click me!</button> </div> ); } }
这样的话其实就可以自定义事件了。
3 this 指向问题
类组件独有的问题?
首先要明确一下,只有类组件才有 this,函数没有 this。所以以下问题都是类组件独有!
其次说一下 this 的绑定问题,是只有回调函数才有的。直接调用的函数是没有的!!
看下面一段和 React 无关的代码
class Person { constructor(name) { this.name = name; } study() { conso.log(this); } } const p1 = new Person('Amy'); p1.study(); const x = p1.study; x(); // 这里就相当于直接调用,那么严格模式下,就是undefined,而函数里面默认是严格模式。
那么在继续看,问一下。这是为什么?
上面的函数这是为什么呢?
关于官方的这个写的事件处理,我以前一直都不不懂为什么她说无法绑定是 JS 本身的问题,而不是 React 的问题。
后来我在看ECMAScript 6 入门-Class 的基本语法 this 的指向 找到了答案。
类的方法内部如果含有this
,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); } } const logger = new Logger(); const { printName } = logger; printName(); // TypeError: Cannot read property 'print' of undefined
上面代码中,printName
方法中的this
,默认指向Logger
类的实例。但是,如果将这个方法提取出来单独使用,this
会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined
),从而导致找不到print
方法而报错。
一个比较简单的解决方法是,在构造方法中绑定this
,这样就不会找不到print
方法了。
class Logger { constructor() { this.printName = this.printName.bind(this); } // ... }
另一种解决方法是使用箭头函数。
class Obj { constructor() { this.getThis = () => this; } } const myObj = new Obj(); myObj.getThis() === myObj; // true
箭头函数内部的this
总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this
会总是指向实例对象。
还有一种解决方法是使用Proxy
,获取方法的时候,自动绑定this
。
function selfish(target) { const cache = new WeakMap(); const handler = { get(target, key) { const value = Reflect.get(target, key); if (typeof value !== 'function') { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); }, }; const proxy = new Proxy(target, handler); return proxy; } const logger = selfish(new Logger());
所以接下来继续在看 React 的事件绑定问题
首先
display()
是作为 onClick 回调,所以不是通过实例调用的,此时的 this。不是上面的类组件的实例,也就是 Home!
<button onClick={this.display}>click me!</button>
那么为什么下面的这个写法有用呢?
render() { return ( <div> hello home <button onClick={this.display()}>no click me!</button> <button onClick={this.display}>click me!</button> <button onClick={this.display.bind(this)}>解决!</button> {/*为什么可以解决?*/} <button onClick={this.displayArrow()}>no click arrow2!</button> <button onClick={this.displayArrow}>click arrow2!</button> </div> ); }
因为当第一次渲染,也就是执行 render()
的时候,此时代码已经执行,当时的 this 就是类组件。
第一次 render 的时候直接执行了这一句话 this.display.bind(this)
,相当于第一次渲染就把当时 this 给了display()
。然后当你执行事件 onClick 的时候,回调函数触发了,这个时候的 this 就是实例组件,而不是 undefined 了。
上面是一个老师说的。
还有一个老师是用源码的角度分析的,所有的事件回调函数,都是 React 给调用的,调用的时候还把 this 绑定成 undefined!
简言之,就是 React 调用回调函数的 this 肯定不是类组件!所以需要你来绑定!
如何绑定 this 的几种方法
几种方案,绑定自定义的函数
第 1 个 显示绑定
第 2 个 构造器绑定
因为一旦执行组件,都会执行构造器。这样先绑定好。
第 3 个 箭头函数
箭头函数从 es6 开始出现,最大的区别,箭头函数中永远不绑定 this,只会找自己最近的上下文 this。也就是当前对象,也就是组件。
第 4 个 表达式内部箭头函数 → 最推荐
下面的逻辑!
使用上面的方法还可以进行传递参数。
下面的代码你可以测试下自己对于 this 绑定的理解
import { Component } from 'react'; class State extends Component { constructor(props) { super(props); this.state = { msg: 'Hello world', }; } handleMsgA() { this.setState({ msg: 'Hello REACT', }); } handleMsgB = () => { this.setState({ msg: 'Hello REACT', }); }; render() { return ( <div> {this.state.msg} <button onClick={this.handleMsgA.bind(this)}> click to change msg A </button> <button onClick={this.handleMsgB}>click to change msg B</button> </div> ); } } export default State;
传递参数
Q:如果你想给事件的回调函数传递一些参数怎么办?
A: 箭头函数回调!
其实就两个事情就是上面的函数不仅仅给绑定了 undefined,还给绑定了 event
4 V18 以后
使用了事件委托
普通调用
import React, { Component } from 'react'; export default class EventDemo1 extends Component { render() { return ( <div> <h2>EventDemo1</h2> <button onClick={() => { console.log('ok EventDemo1'); }} > click it! </button> </div> ); } }
那么写在一个函数里
import React, { Component } from 'react'; export default class EventDemo2 extends Component { handle = function () { // 为什么是undefined // 因为调用这个handle的并不是这个EventDemo2对象实例 // 而是React内部某个机制调用的 console.log(this); // undefined }; render() { return ( <div> <h2>EventDemo2</h2> <button onClick={this.handle}>click it!</button> </div> ); } }
基于 React 内部处理 和合成事件绑定一个普通函数 当事件出发 函数执行
那么如何解决呢?就是使用 bind 改变下指向,只用 call 和 apply 都是需要直接执行的。为什么此时 this 是实例呢?因为这是由上下文决定的
render 是谁执行呢?也是 react 执行的。不过是实例。
解决方法 1
import React, { Component } from 'react'; export default class EventDemo3 extends Component { handle = function () { console.log(this); }; render() { return ( <div> <h2>EventDemo3</h2> {/* 只要在此时绑定this就行 */} {/* 因为你绑定的时候 此时的this就是实例 要记住 此时还没触发这个事件 此时的this就是实例 当你触发的时候this已经绑定了 */} <button onClick={this.handle.bind(this)}>click it!</button> </div> ); } }
解决方法 2 使用箭头函数
import React, { Component } from 'react'; export default class EventDemo4 extends Component { handle = () => { // 使用箭头函数 此时的this在箭头函数定义时就确定了 // 推荐用这个 console.log(this); }; render() { return ( <div> <h2>EventDemo4</h2> <button onClick={this.handle}>click it!</button> </div> ); } }
如果我想拿到原生事件呢?
import React, { Component } from 'react'; export default class EventDemo5 extends Component { // 这里可以明确的缺点 此时事件触发是由React发起的 // 此时会给你传递一个实参,e包括了所有的合成事件对象 也包括了原生的事件 handle = (e) => { console.log(e); console.log(e.nativeEvent); // 可以获取浏览器原生事件对象 }; render() { return ( <div> <h2>EventDemo5</h2> <button onClick={this.handle}>click it!</button> </div> ); } }
经过 bind 处理才是合成事件对象
import React, { Component } from 'react'; export default class EventDemo6 extends Component { // 这个合成事件被放在了最后,即使你写在了前面的 handle = function (ev, x, y) { console.log(x); console.log(y); console.log(ev); }; // 最后一个参数才是真正的event对象 // handle = function (x, y, ev) { // console.log(x); // console.log(y); // console.log(ev); // }; render() { return ( <div> <h2>EventDemo6</h2> <button onClick={this.handle.bind(this, 10, 20)}>click it!</button> </div> ); } }
也就是说 bind 有 2 个用法
- 绑定 this 为实例对象
- 传递参数 此时事件对象是最后一个参数
<button onClick={this.handle.bind(this)}>click it!</button> {/*改变this*/} <button onClick={this.handle.bind(null, 10, 20)}>click it!</button> {/*传递参数*/}
事件委托 Event Delegation
这个要补充下基础知识,就是先知道下 JS 的基础知识,事件委托。
捕获阶段 目标阶段 冒泡阶段 这个捕获机制要明白
关于 stop 和 stop Immediate
利用事件的传播机制,实现了一套事件绑定。
本质是什么呢?
// 所谓合成事件绑定 // 没有给元素本身做事件绑定 // 而是给元素设置了onXXX/onXXXPature这样的合成事件属性 // 当事件行为触发的时候,都会传播到#root事件身上 由root绑定 捕获 冒泡 // 本质就是最后根据ev.path分析的路径 进行事件绑定 // #给root做事件绑定 root.addEventListener('click', () => { let path = ev.path; [...path].reverse().forEach((ele) => { let handle = ele.onClickCapture; if (handle) handle(); // 这里也可以解释为什么this是undefined的疑惑 因为内部就是直接调用的 }); }); root.addEventListener('click', () => { let ev = ev.path; path.forEach((ele) => { let handle = ele.onClickCapture; if (handle) handle(); }); });
本质
这个根据版本不同是不一样的,这个要去看。
- v16
- v17
- v18
5 ChatGPT 给的一些回答
在 React 中,合成事件(Synthetic Event) 是对原生浏览器事件的封装,使其具有跨浏览器的兼容性,同时提供了额外的优化。
1. 为什么需要合成事件?
在原生 JavaScript 事件处理中,我们通常使用 addEventListener 监听事件,但不同浏览器对事件的实现存在差异。React 为了解决这个问题,引入了 合成事件(SyntheticEvent)
- 提供跨浏览器兼容性,避免手动处理不同浏览器的事件差异。
- 事件对象会被自动回收,减少内存消耗,提高性能。
- React 使用事件代理(Event Delegation),在 document 级别监听所有事件,提高性能。
2. 如何使用合成事件
在 React 组件中,合成事件的使用方式类似于原生事件,但它们是 React 提供的封装对象。
✅ 基本示例
import React from "react"; function App() { const handleClick = (event) => { console.log("点击了按钮!", event); }; return <button onClick={handleClick}>点击我</button>; } export default App;
🔹 特点:
onClick={handleClick}
绑定事件处理函数,handleClick 会接收一个 合成事件对象(SyntheticEvent),类似于 event 但由 React 提供。
4. 事件池(Event Pool)
React 采用 事件池(Event Pool) 机制,事件对象会在事件回调函数执行完毕后被回收,以减少内存消耗。
✅ 示例
function App() { const handleClick = (event) => { console.log(event); // 事件对象 setTimeout(() => { console.log(event); // 可能是 null(因为事件被回收) }, 1000); }; return <button onClick={handleClick}>点击我</button>; } export default App;
🔍 可能的错误
Warning: This synthetic event is reused for performance reasons. If you’re seeing this, you’re accessing the event properties asynchronously.
解决方案:使用 event.persist(),阻止事件对象被回收:
const handleClick = (event) => { event.persist(); // 保持事件对象 setTimeout(() => { console.log(event); // 仍然有效 }, 1000); };
5. React 支持的合成事件
React 处理的事件类型与标准 DOM 事件类似,例如:
• 鼠标事件:onClick,onDoubleClick,onMouseEnter,onMouseLeave
• 键盘事件:onKeyDown,onKeyPress,onKeyUp
• 表单事件:onChange,onSubmit,onFocus
• 触摸事件:onTouchStart,onTouchMove
• 剪贴板事件:onCopy,onCut,onPaste
✅ 事件示例
function App() { const handleChange = (event) => { console.log('输入值:', event.target.value); }; const handleKeyDown = (event) => { console.log('按下按键:', event.key); }; return ( <div> <input type="text" onChange={handleChange} onKeyDown={handleKeyDown} /> </div> ); } export default App;
6. 事件委托(Event Delegation)
React 使用 事件委托,即 所有事件默认绑定在 document 上,而不是直接绑定到组件的 DOM 元素。这样做的好处:
• 减少内存消耗:不用给每个元素绑定事件。
• 动态元素可以自动继承事件,比如 .map() 渲染的列表项。
✅ 事件委托示例
function App() { const handleClick = (event) => { console.log('点击了:', event.target.textContent); }; return ( <div onClick={handleClick}> <button>按钮 1</button> <button>按钮 2</button> </div> ); } export default App;
🔹 点击任意按钮,事件都会触发 handleClick,React 并不会给每个按钮单独绑定事件,而是利用 事件委托机制。
事件绑定的不同方式
在 React 组件中,我们可以用以下几种方式绑定事件:
✅ 方式 1:直接在 JSX 内联
<button onClick={() => console.log('点击了按钮')}>点击</button>
⚠️ 缺点:每次渲染都会创建一个新的函数,不推荐在高性能应用中使用。
✅ 方式 2:绑定到组件方法(推荐)
function App() { const handleClick = () => { console.log('点击了按钮'); }; return <button onClick={handleClick}>点击</button>; }
✅ 优点:每次渲染不会创建新的函数,提高性能。
✅ 方式 3:使用 bind(this)(仅限类组件)
class App extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log('点击了按钮'); } render() { return <button onClick={this.handleClick}>点击</button>; } }
⚠️ 类组件推荐使用箭头函数来避免 bind:
class App extends React.Component { handleClick = () => { console.log('点击了按钮'); }; render() { return <button onClick={this.handleClick}>点击</button>; } }
总结
-
React 的合成事件(SyntheticEvent)是对原生事件的封装,保证跨浏览器兼容性。
-
事件对象会被回收,避免内存泄漏,但可以用 event.persist() 解决异步问题。
-
React 使用事件委托,将事件绑定在 document 以提高性能。
-
推荐使用函数组件和 Hooks 处理事件,避免 bind(this) 问题。
共有评论(0)