React:功能组件内部还是外部的事件处理程序?

Yui*_*Yui 5 javascript event-handling node.js reactjs react-functional-component

考虑 React 中这个简单的 TextField 组件:

import React, { useState } from "react";
import { TextField, Grid } from "@mui/material";

const handleEnter = (event) => {
    console.log("In handleEnter");
    if (event.key == "Enter" && event.shiftKey) {
        console.log("Detected Shift+Enter key");
    } else if (event.key == "Enter") {
        console.log("Detected Enter key");
    }
};

export default function Example() {
    const [value, setValue] = React.useState("");

    const handleChatBoxChange = (event) => {
        setValue(event.target.value);
    };

    return (
        <Grid container>
            <TextField
                id='chatBox'
                maxRows={90}
                onKeyDown={handleEnter}
                value={value}
                onChange={handleChatBoxChange}
                variant='filled'
            ></TextField>
        </Grid>
    );
}
Run Code Online (Sandbox Code Playgroud)

这是一个非常简单的组件,handleEnter会检测用户何时按下 Enter 或 Shift+Enter 组合键。我意识到handleEnter的实现可以写在函数Example()之外,也可以写在Example()内部。但是,handleChatBoxChange() 不能放置在外部,因为它依赖于通过 useState() 创建的 setValue(并且 useState() 必须位于功能组件内部。对此的最佳实践是什么?

jse*_*ksn 3

简洁的答案:“只要可行就在外面”

继续阅读原因...


在这个答案中,我忽略了代码风格和源代码模块管理(这是意见)。

当您阅读此答案时,需要理解的一些重要概念是纯函数闭包“不纯”函数(不属于其他两类的所有函数)之间的区别。Stack Overflow 上已经对此进行了广泛介绍,因此我不会在这里重复。


除了您对闭包本质的观察之外,对引用相等(对象标识)的影响也非常不同。考虑以下:

在下面的代码中(您可以想象它是一个名为 的模块文件component.jsx),回调函数是在模块的顶层、组件外部创建的。它在程序运行时初始化一次,并且其值在 JS 内存的生命周期内不会改变:

const handleClick = ev => {/*...*/};

const Component = () => {
  return <div onClick={handleClick}></div>;
};
Run Code Online (Sandbox Code Playgroud)

相反,下面的代码显示了在组件内部创建的回调函数。因为组件本身只是函数,这意味着每次 React 调用该组件时,都会重新创建回调函数(一个全新的函数,不等于先前渲染中的“自身版本”):

const Component = () => {
  const handleClick = ev => {/*...*/};
  return <div onClick={handleClick}></div>;
};
Run Code Online (Sandbox Code Playgroud)

在上述场景中,函数直接用作 child 的事件处理程序回调ReactElement,不会产生明显的差异。然而,当将函数传递给子组件或在钩子的回调函数中使用它时useEffect,事情会变得更加复杂:

import {useEffect} from 'react';

const handleClick = ev => {/*...*/};

const Component = () => {
  useEffect(() => {
    const message = `The name of the function is ${
      handleClick.name
    }, and this message will only appear in the console when this component first mounts`;
    console.log(message);
  }, []);

  return <div onClick={handleClick}></div>;
};
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,该函数不需要包含在效果的依赖项列表中,因为它的值不能也永远不会改变。它不是在组件内部创建的,也不是从 props 接收的,因此引用将是稳定的。

相反,考虑这个例子:

import {useEffect} from 'react';

const Component = () => {
  const handleClick = ev => {/*...*/};

  useEffect(() => {
    const message = `The name of the function is ${
      handleClick.name
    }, and this message will appear in the console EVERY time this component renders`;
    console.log(message);
  }, [handleClick]);

  return <div onClick={handleClick}></div>;
};
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,每次在渲染期间调用组件时都会重新创建该函数,从而导致效果检测到依赖项数组中的不同值并再次运行该效果。正是由于这个原因,需要将该函数包含在依赖项数组中。

当将函数作为 props 传递给子组件时,同样的概念也适用。当组件接收函数作为 prop 值时(prop 值只是函数组件的对象参数上的属性值),它无法了解有关该函数的引用稳定性的任何信息,因此该函数必须始终包含在使用它的依赖项列表中。

这引出了记忆化的概念和相关的内置钩子(例如useMemouseCallback)。(与函数类型类似,记忆化是一个更大的主题,并且已经在 SO 和许多其他地方进行了介绍。)

在结束这个答案之前,我将给出一个实际示例,说明如何为闭包函数创建稳定的对象标识:

import {useCallback, useState} from 'react';

const Component = () => {
  const [count, setCount] = useState(0);

  const adjustCount = useCallback(
    (amount) => setCount(count => count + amount),
    [setCount],
  // ^^^^^^^^
  ); // It's optional to include the "setState" function provided by `useState`,
  // in the dependency array here (it's a special case exception because React
  // already guarantees that the "setState" function it gives you will remain
  // stable). However, it doesn't hurt to include it (although you might
  // choose to omit it if you're working on a refactor toward
  // performance micro-optimizations).

  // Now, when you pass that function to a child component as a prop value,
  // the object identity won't change on subsequent renders:
  //                                      vvvvvvvvvvv
  return <SomeChildComponent adjustCount={adjustCount} />;
};
Run Code Online (Sandbox Code Playgroud)