提问人:user573 提问时间:10/13/2023 最后编辑:Nick Parsonsuser573 更新时间:10/13/2023 访问量:92
为什么将变量绑定到事件侦听器回调会导致它再次被定义?这是可以避免的吗?
Why does binding variables to an event listener callback cause it to be defined again? Can this be avoided?
问:
很抱歉,如果这有点粗糙,我对 React 比较陌生。这甚至可能更像是一个 JavaScript 问题。
我正在尝试添加一个事件侦听器,该侦听器将在组件内触发回调。这个组件可以在页面上多次出现,使用下面的代码,当单击时,将输出一次 - 我可以添加任意数量的组件,并且日志将只输出一次 - 根据需要。#btn
console.log
<MyComponent />
const callback = (e) => {
console.log('callback happened!!', e.type);
}
const MyComponent = () => {
const btn = document.getElementById('btn');
if (btn) {
const name = 'Bob';
btn.addEventListener('click', callback);
}
return (
<div>
<p>Hi from my component!</p>
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
<MyComponent />
<p>...</p>
<MyComponent />
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
<div id="btn">button</div>
我遇到的问题是,如果我尝试使用 bind 将变量 () 传递给回调函数(例如),当我单击该按钮时,输出将被记录两次 - 这不是我想要的!我需要变量和事件对象在函数中可用。name
btn.addEventListener('click', callback.bind(null, name));
name
callback
我应该澄清一下,在上面的示例中,我在按钮上使用单击侦听器,但实际情况将侦听从其他内容发出的事件。
我尝试将回调函数移动到类中,并将其作为道具传递给组件,但同样的事情发生了 - 一旦我将变量绑定到它,它就会触发控制台日志两次。App
所以,我的问题是,为什么会这样?如何实现这些要求?
欢迎所有建议,谢谢!
答:
这里的问题是,在创建组件时,您正在创建事件侦听器,但您永远不会删除它。这意味着每次实例化组件时,都会创建一个新的事件侦听器,因此将显示一个新的控制台 .log。
此外,你正在以一种奇怪的方式做事:
- 不要使用 React 类组件,功能组件是推荐的选择;
- 为什么要使用 id 创建按钮,然后在组件内部获取按钮引用?React 的优点是你不必这样做,你只需将组件插入到所在的位置即可。
<div id="btn">button</div>
我将为你提供两种解决方案,第一种使用类组件,以便更接近你的解决方案(即使不建议再使用它们),另一种使用功能组件:
类组件:
import React from "react"
import ReactDOM from "react-dom"
class MyComponent extends React.Component {
handleClick = (name, e) => {
console.log("callback happened!!", e.type, name)
}
render() {
const { name } = this.props
return (
<div>
<p onClick={(e) => this.handleClick(name, e)}>Hi from my component!</p>
</div>
)
}
}
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
<MyComponent name="Bob" />
<p>...</p>
<MyComponent name="Alice" />
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
功能部件:
import React from "react"
import ReactDOM from "react-dom"
function MyComponent({ name }) {
const handleClick = (e) => {
console.log("callback happened!!", e.type, name)
}
return (
<div>
<p onClick={(e) => handleClick(e)}>Hi from my component!</p>
</div>
)
}
function App() {
return (
<div>
<MyComponent name="Bob" />
<p>...</p>
<MyComponent name="Alice" />
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
请注意,在这两种解决方案中,我都没有显式创建事件侦听器,但我在元素的属性中附加了必须在单击时调用的回调函数。onClick
另一方面,如果您必须添加事件侦听器,那么在组件卸载时将其删除非常重要。
类组件:
import React from 'react';
import ReactDOM from 'react-dom';
const callback = (name) => (e) => {
console.log('callback happened!!', e.type, name);
};
class MyComponent extends React.Component {
componentDidMount() {
const btn = document.getElementById('btn');
if (btn) {
btn.addEventListener('click', callback(this.props.name));
}
}
componentWillUnmount() {
const btn = document.getElementById('btn');
if (btn) {
btn.removeEventListener('click', callback(this.props.name));
}
}
render() {
return (
<div>
<p>Hi from my component!</p>
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<MyComponent name="Bob" />
<p>...</p>
<MyComponent name="Alice" />
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('#app'));
功能部件:
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
const callback = (name) => (e) => {
console.log('callback happened!!', e.type, name);
};
function MyComponent(props) {
useEffect(() => {
const btn = document.getElementById('btn');
if (btn) {
btn.addEventListener('click', callback(props.name));
return () => {
btn.removeEventListener('click', callback(props.name));
};
}
}, [props.name]);
return (
<div>
<p>Hi from my component!</p>
</div>
);
}
function App() {
return (
<div>
<MyComponent name="Bob" />
<p>...</p>
<MyComponent name="Alice" />
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
希望这是有用的!
评论
你已经说过按钮是另一种事件源的替代品,所以我们不会担心像这样的附加点击处理程序不是你在 React 中通常的做法。我假设另一个事件源在 React 树之外。
每次需要渲染组件时都会调用组件函数,可以多次调用。每次函数运行时,代码都会添加一个处理程序。当您直接使用该函数执行此操作时,它每次都是相同的函数,因此不会添加(因为不会多次将同一事件的相同函数添加到同一事件目标,即使您重复调用它也是如此)。但是当你使用 时,你每次都会创建一个新函数,因此在每次渲染时都会添加这些新函数。callback
addEventListener
bind
addEventListener
相反,只有在安装组件时才进行设置。此外,在卸载组件时将其删除。你可以通过 useEffect
来做到这一点:
const MyComponent = () => {
useEffect(() => {
const btn = document.getElementById("btn");
if (btn) {
const name = "Bob";
const handler = callback.bind(null, name);
// Or: `const handler = (event) => callback(name, event);`
btn.addEventListener("click", handler);
return () => {
// This function is called to clean up
btn.removeEventListener("click", handler);
};
}
}, []); // <== Empty array means "only on mount"
return (
<div>
<p>Hi from my component!</p>
</div>
);
}
const { useEffect } = React;
const callback = (name, e) => {
console.log(`Callback happened!! type = ${e.type}, name = ${name}`);
};
const MyComponent = ({ name }) => {
useEffect(() => {
console.log(`MyComponent "${name}": Mounted`);
const btn = document.getElementById("btn");
if (btn) {
// Using a prop here instead of a constant so we can tell each
// component instance is calling the callback
const handler = callback.bind(null, name);
// Or: `const handler = (event) => callback(name, event);`
btn.addEventListener("click", handler);
return () => {
// This function is called to clean up
btn.removeEventListener("click", handler);
};
}
}, []); // <== Empty array means "only on mount"
console.log(`MyComponent "${name}": Rendering`);
return (
<div>
<p>Hi from my component! name = {name}</p>
</div>
);
};
// Note: The React team considedr `class` components "legacy;" new code should use function components
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
};
}
render() {
const increment = () =>
this.setState(({ counter }) => ({ counter: counter + 1 }));
const { counter } = this.state;
return (
<div>
<div>
Click the counter to see that the handler isn't added on
every render: {counter}{" "}
<input type="button" value="+" onClick={increment} />
</div>
<MyComponent name="first" />
<p>...</p>
<MyComponent name="second" />
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#app"));
<input type="button" id="btn" value="button">
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
评论
btn.addEventListener('click', callback);
将在呈现 MyComponent 时被调用的次数。因此,您在 btn 上有多个侦听器。Idk 你的用例,但看起来你应该有一个单例方法或一些有持久性的方法。此外,您可能还想分享您的真实用例.bind()