发布于 2026-01-06 6 阅读
0

React DEV 全球展示挑战赛:Mux 呈现的“编写事件处理函数的 5 个关键技巧:展示你的项目!”

React 中编写事件处理函数的 5 个关键技巧

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

在Medium上找到我

JavaScript 因其独特的函数编写和创建方式而备受赞誉。这是因为在 JavaScript 中,函数是“一等公民”,这意味着它们可以像值一样被对待,并拥有与其他类型相同的操作属性,例如可以赋值给变量、作为函数参数传递、作为函数返回值等等。

我们将介绍在 React 中编写事件处理程序的 5 个关键技巧。本文不会涵盖所有可能的情况,但会介绍每个 React 开发人员至少应该了解的编写事件处理程序的重要方法!

我们将从一个输入元素开始,并为其添加一个value属性onChange

import React from 'react'
import './styles.css'

function MyInput() {
  const [value, setValue] = React.useState('')

  function onChange(e) {
    setValue(e.target.value)
  }

  return (
    <div>
      <input type='text' value={value} onChange={onChange} />
    </div>
  )
}

export default MyInput
Enter fullscreen mode Exit fullscreen mode

我们的事件处理程序是,onChange第一个参数是来自附加了该处理程序的元素的事件对象。

我们还能从哪里改进呢?通常来说,编写可重用的组件是一种很好的做法,我们可以让这个组件也具备可重用性。

1. 将二传手移到更高层级

一种方法是将设置状态的责任移交valueprops其他组件,以便其他组件可以重用此输入:

import React from 'react'
import MyInput from './MyInput'

function App() {
  const [value, setValue] = React.useState('')

  return <MyInput value={value} />
}

export default App
Enter fullscreen mode Exit fullscreen mode

这意味着我们还必须将事件处理程序(其中包含状态设置器)的控制权交给父级:

function App() {
  const [value, setValue] = React.useState('')

  function onChange(e) {
    setValue(e.target.value)
  }

  return <MyInput value={value} onChange={onChange} />
}
Enter fullscreen mode Exit fullscreen mode
function MyInput({ value, onChange }) {
  return (
    <div>
      <input type='text' value={value} onChange={onChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

但我们所做的只是将状态和事件处理程序移到了父组件,最终我们的App组件与之前的组件完全相同MyInput,只是名称不同。那么,这样做的意义何在呢?

2. 如果出于可扩展性目的需要更多信息,请封装您的事件处理程序。

当我们开始进行组件组合时,情况就开始发生变化。看看这个MyInput组件。我们不再直接给onChange它的input元素赋值,而是可以为这个可复用的组件添加一些额外的功能,使其更加实用。

我们可以onChange通过将其组合到另一个 onChange 事件中来操控它,并将新的事件附加onChange到元素上。在新的事件内部,onChange它会从 props 中调用原始事件onChange,这样功能仍然可以正常运行——就像什么都没发生过一样。

举个例子:

function MyInput({ value, onChange: onChangeProp }) {
  function onChange(e) {
    onChangeProp(e)
  }

  return (
    <div>
      <input type='text' value={value} onChange={onChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

value这使得在代码变更时能够注入额外的逻辑,非常强大input。它的行为正常,因为它仍然会在其代码块内调用原始方法onChange

例如,我们现在可以强制输入元素只接受数字值,并且最多只能接受 6 个字符的长度,如果您想用它来验证用户手机上的登录信息,这将非常有用:

function isDigits(value) {
  return /^\d+$/.test(value)
}

function isWithin6(value) {
  return value.length <= 6
}

function MyInput({ value, onChange: onChangeProp }) {
  function onChange(e) {
    if (isDigits(e.target.value) && isWithin6(e.target.value)) {
      onChangeProp(e)
    }
  }

  return (
    <div>
      <input type='text' value={value} onChange={onChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

但实际上,App到目前为止,所有这些操作都可以在父级中顺利实现。但是,如果onChange父级中的处理程序需要的不仅仅是事件对象呢?那么父级中的处理程序就失去了作用:MyInputonChange

function App() {
  const [value, setValue] = React.useState('')

  function onChange(e) {
    setValue(e.target.value)
  }

  return <MyInput value={value} onChange={onChange} />
}
Enter fullscreen mode Exit fullscreen mode

但是,App除了事件对象和知道元素的值正在改变之外,还能需要什么呢onChange?而它既然处于处理程序的执行上下文中,就已经知道这一点了。

3. 利用通过参数组成的原始处理程序

直接访问元素input本身非常有用。这意味着最好将某个ref对象与事件对象一起传递。由于onChange处理程序已在此处编写,因此很容易实现:

function MyInput({ value, onChange: onChangeProp }) {
  function onChange(e) {
    if (isDigits(e.target.value) && isWithin6(e.target.value)) {
      onChangeProp(e)
    }
  }

  return (
    <div>
      <input type='text' value={value} onChange={onChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

我们只需要声明 React Hook useRef,将其附加到 `<script>`input标签上,并将其作为第二个参数传递给 `<script>` 标签onChangeProp,以便调用者可以访问它:

function MyInput({ value, onChange: onChangeProp }) {
  const ref = React.useRef()

  function onChange(e) {
    if (isDigits(e.target.value) && isWithin6(e.target.value)) {
      onChangeProp(e, { ref: ref.current })
    }
  }

  return (
    <div>
      <input ref={ref} type='text' value={value} onChange={onChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
function App() {
  const [value, setValue] = React.useState('')

  function onChange(e, { ref }) {
    setValue(e.target.value)

    if (ref.type === 'file') {
      // It's a file input
    } else if (ref.type === 'text') {
      // Do something
    }
  }

  return (
    <div>
      <MyInput value={value} onChange={onChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

4. 保持高阶函数处理程序和复合处理程序的签名相同

通常来说,保持组合函数的签名与原始函数的签名一致非常onChange重要。我的意思是,在我们的示例中,两个处理程序的第一个参数都保留给事件对象。

在组合函数时保持函数签名相同有助于避免不必要的错误和混淆。

如果我们像这样交换参数的位置:

JavaScript React 事件处理程序中参数位置互换

这样一来,当我们重用组件时,就很容易忘记这一点并导致出错:

function App() {
  const [value, setValue] = React.useState('')

  function onChange(e, { ref }) {
    // ERROR --> e is actually the { ref } object so e.target is undefined
    setValue(e.target.value)
  }

  return (
    <div>
      <MyInput value={value} onChange={onChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

避免这种混淆,对您和其他开发人员来说压力也会减轻。

一个很好的例子是,当你想允许调用者提供任意数量的事件处理程序,同时又想保证应用程序正常运行时:

const callAll = (...fns) => (arg) => fns.forEach((fn) => fn && fn(arg))

function MyInput({ value, onChange, onChange2, onChange3 }) {
  return (
    <input
      type='text'
      value={value}
      onChange={callAll(onChange, onChange2, onChang3)}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

如果其中至少有一个函数尝试执行某些特定于字符串的方法.concat,例如 `\t`,则会发生错误,因为函数签名是 `\t`function(event, ...args)而不是 `\t` function(str, ...args)

function App() {
  const [value, setValue] = React.useState('')

  function onChange(e, { ref }) {
    console.log(`current state value: ${value}`)
    console.log(`incoming value: ${e.target.value}`)
    setValue(e.target.value)
    console.log(`current state value now: ${value}`)
  }

  function onChange2(e) {
    e.concat(['abc', {}, 500])
  }

  function onChange3(e) {
    console.log(e.target.value)
  }

  return (
    <div>
      <MyInput
        value={value}
        onChange={onChange}
        onChange2={onChange2}
        onChange3={onChange3}
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

signature-change-event-handler-javascript-react

5. 避免在事件处理程序(闭​​包)内部引用和依赖状态。

这样做真的很危险!

如果操作得当,在回调处理程序中管理状态应该不会有问题。但如果你在某个环节出现疏忽,导致引入难以调试的隐性 bug,那么后果就会开始显现,耗费你大量宝贵的时间,让你后悔不已。

如果你正在做类似这样的事情:

function onChange(e, { ref }) {
  console.log(`current state value: ${value}`)
  console.log(`incoming value: ${e.target.value}`)
  setValue(e.target.value)
  console.log(`current state value now: ${value}`)
}
Enter fullscreen mode Exit fullscreen mode

您最好重新检查一下这些处理程序,看看是否真的得到了预期的结果。

如果我们的input值为"23",我们"3"在键盘上输入另一个值,结果如下:

事件处理程序不同步状态

setValue如果你了解 JavaScript 中的执行上下文,就会明白这毫无意义,因为在执行下一行之前,对 `to` 的调用已经执行完毕了!

没错,这说法仍然正确。JavaScript 现在没有任何错误之处。实际上是React在按部就班地运行。

如需了解渲染过程的完整说明,您可以前往他们的文档

简而言之,基本上每当 React 进入一个新的渲染阶段时,它都会对该渲染阶段的所有内容进行“快照”。在这个阶段,React 会创建一个 React 元素树,该元素树代表了当时的页面结构

根据定义,调用setValue 确实会导致重新渲染,但该渲染阶段发生在未来的某个时间点!这就是为什么在执行完毕后状态value仍然保持不变,因为此时的执行是特定于该渲染的,就像它们生活在一个独立的小世界里一样。23setValue

这就是 JavaScript 中执行上下文的概念:

javascript执行上下文图流程图

这是我们示例中 React 的渲染阶段(您可以将其理解为 React 有自己的执行上下文):

react渲染阶段协调流程图

综上所述,让我们再次审视一下我们的呼吁setCollapsed

React渲染阶段状态更新

在 React2 的渲染阶段更新状态

这一切都发生在同一个渲染阶段,所以`collapsed`属性仍然有效true,并且person被传递为 ` null.`。当整个组件重新渲染时,下一个渲染阶段的值将代表前一个渲染阶段的值:

react-state-update-reconciliation2

在Medium上找到我

文章来源:https://dev.to/jsmanifest/5-ritic-tips-for-composition-event-handler-functions-in-react-1ai