为什么将变量绑定到事件侦听器回调会导致它再次被定义?这是可以避免的吗?

Why does binding variables to an event listener callback cause it to be defined again? Can this be avoided?

提问人:user573 提问时间:10/13/2023 最后编辑:Nick Parsonsuser573 更新时间:10/13/2023 访问量:92

问:

很抱歉,如果这有点粗糙,我对 React 比较陌生。这甚至可能更像是一个 JavaScript 问题。

我正在尝试添加一个事件侦听器,该侦听器将在组件内触发回调。这个组件可以在页面上多次出现,使用下面的代码,当单击时,将输出一次 - 我可以添加任意数量的组件,并且日志将只输出一次 - 根据需要。#btnconsole.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 将变量 () 传递给回调函数(例如),当我单击该按钮时,输出将被记录两次 - 这不是我想要的!我需要变量和事件对象在函数中可用。namebtn.addEventListener('click', callback.bind(null, name));namecallback

我应该澄清一下,在上面的示例中,我在按钮上使用单击侦听器,但实际情况将侦听从其他内容发出的事件。

我尝试将回调函数移动到类中,并将其作为道具传递给组件,但同样的事情发生了 - 一旦我将变量绑定到它,它就会触发控制台日志两次。App

所以,我的问题是,为什么会这样?如何实现这些要求?

欢迎所有建议,谢谢!

JavaScript ReactJS 回调 addEventListener

评论

2赞 Tushar Shahi 10/13/2023
btn.addEventListener('click', callback);将在呈现 MyComponent 时被调用的次数。因此,您在 btn 上有多个侦听器。Idk 你的用例,但看起来你应该有一个单例方法或一些有持久性的方法。此外,您可能还想分享您的真实用例
1赞 T.J. Crowder 10/13/2023
“......但实际情况将是侦听从其他东西发出的事件......“ 组件的所有实例都应该对事件做出反应吗?
0赞 Nick Parsons 10/13/2023
请注意,在呈现函数组件时,会多次添加相同的函数引用,因此只添加了一个事件侦听器(因为它是相同的函数引用,这就是你看到一个日志的原因)。使用时,每次都会创建一个新函数,从而导致同时显示两个日志。.bind()

答:

1赞 Domiziano Scarcelli 10/13/2023 #1

这里的问题是,在创建组件时,您正在创建事件侦听器,但您永远不会删除它。这意味着每次实例化组件时,都会创建一个新的事件侦听器,因此将显示一个新的控制台 .log。

此外,你正在以一种奇怪的方式做事:

  1. 不要使用 React 类组件,功能组件是推荐的选择;
  2. 为什么要使用 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'));

希望这是有用的!

评论

1赞 user573 10/13/2023
这很棒,绝对是食物,谢谢。是的,对不起,这只是 JS 小提琴的一个粗略示例 - 结构有点垃圾,但这只是一个粗略的想法。我听过一些人提到功能组件现在比经典组件更好——你知道关于这个主题的好书吗?
1赞 pilchard 10/13/2023
请参阅官方文档:React: Component,它以“我们建议将组件定义为函数而不是类”的注释开头。
3赞 T.J. Crowder 10/13/2023 #2

你已经说过按钮是另一种事件源的替代品,所以我们不会担心像这样的附加点击处理程序不是你在 React 中通常的做法。我假设另一个事件源在 React 树之外。

每次需要渲染组件都会调用组件函数,可以多次调用。每次函数运行时,代码都会添加一个处理程序。当您直接使用该函数执行此操作时,它每次都是相同的函数,因此不会添加(因为不会多次将同一事件的相同函数添加到同一事件目标,即使您重复调用它也是如此)。但是当你使用 时,你每次都会创建一个新函数,因此在每次渲染时都会添加这些函数。callbackaddEventListenerbindaddEventListener

相反,只有在安装组件时才进行设置。此外,在卸载组件时将其删除。你可以通过 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>

评论

0赞 user573 10/13/2023
你的假设是正确的!感谢您的回答,这都是很棒的信息 - 我今天一定会探索 useEffect 的使用。至于组件的挂载/卸载,我对此仍然有点困惑,需要做一些阅读......不过,这绝对是朝着正确方向前进的指针!:)