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
我们的事件处理程序是,onChange第一个参数是来自附加了该处理程序的元素的事件对象。
我们还能从哪里改进呢?通常来说,编写可重用的组件是一种很好的做法,我们可以让这个组件也具备可重用性。
1. 将二传手移到更高层级
一种方法是将设置状态的责任移交value给props其他组件,以便其他组件可以重用此输入:
import React from 'react'
import MyInput from './MyInput'
function App() {
const [value, setValue] = React.useState('')
return <MyInput value={value} />
}
export default App
这意味着我们还必须将事件处理程序(其中包含状态设置器)的控制权交给父级:
function App() {
const [value, setValue] = React.useState('')
function onChange(e) {
setValue(e.target.value)
}
return <MyInput value={value} onChange={onChange} />
}
function MyInput({ value, onChange }) {
return (
<div>
<input type='text' value={value} onChange={onChange} />
</div>
)
}
但我们所做的只是将状态和事件处理程序移到了父组件,最终我们的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>
)
}
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>
)
}
但实际上,App到目前为止,所有这些操作都可以在父级中顺利实现。但是,如果onChange父级中的处理程序需要的不仅仅是事件对象呢?那么,父级中的处理程序就失去了作用:MyInputonChange
function App() {
const [value, setValue] = React.useState('')
function onChange(e) {
setValue(e.target.value)
}
return <MyInput value={value} onChange={onChange} />
}
但是,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>
)
}
我们只需要声明 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>
)
}
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>
)
}
4. 保持高阶函数处理程序和复合处理程序的签名相同
通常来说,保持组合函数的签名与原始函数的签名一致非常onChange重要。我的意思是,在我们的示例中,两个处理程序的第一个参数都保留给事件对象。
在组合函数时保持函数签名相同有助于避免不必要的错误和混淆。
如果我们像这样交换参数的位置:
这样一来,当我们重用组件时,就很容易忘记这一点并导致出错:
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>
)
}
避免这种混淆,对您和其他开发人员来说压力也会减轻。
一个很好的例子是,当你想允许调用者提供任意数量的事件处理程序,同时又想保证应用程序正常运行时:
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)}
/>
)
}
如果其中至少有一个函数尝试执行某些特定于字符串的方法.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>
)
}
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}`)
}
您最好重新检查一下这些处理程序,看看是否真的得到了预期的结果。
如果我们的input值为"23",我们"3"在键盘上输入另一个值,结果如下:
setValue如果你了解 JavaScript 中的执行上下文,就会明白这毫无意义,因为在执行下一行之前,对 `to` 的调用已经执行完毕了!
没错,这说法仍然正确。JavaScript 现在没有任何错误之处。实际上是React在按部就班地运行。
如需了解渲染过程的完整说明,您可以前往他们的文档。
简而言之,基本上每当 React 进入一个新的渲染阶段时,它都会对该渲染阶段的所有内容进行“快照”。在这个阶段,React 会创建一个 React 元素树,该元素树代表了当时的页面结构。
根据定义,调用setValue 确实会导致重新渲染,但该渲染阶段发生在未来的某个时间点!这就是为什么在执行完毕后状态value仍然保持不变,因为此时的执行是特定于该渲染的,就像它们生活在一个独立的小世界里一样。23setValue
这就是 JavaScript 中执行上下文的概念:
这是我们示例中 React 的渲染阶段(您可以将其理解为 React 有自己的执行上下文):
综上所述,让我们再次审视一下我们的呼吁setCollapsed:
这一切都发生在同一个渲染阶段,所以`collapsed`属性仍然有效true,并且person被传递为 ` null.`。当整个组件重新渲染时,下一个渲染阶段的值将代表前一个渲染阶段的值:
在Medium上找到我
文章来源:https://dev.to/jsmanifest/5-ritic-tips-for-composition-event-handler-functions-in-react-1ai







