在我的工作中,我经常遇到一个事实,即开发人员并不总是清楚地了解如何通过道具(尤其是回调)传输数据的机制,以及为什么如此频繁地更新其PureComponents。
因此,在本文中,我们将了解如何将回调传递给React,并讨论事件处理程序的功能。
TL; DR
- 不要干扰JSX和业务逻辑-这会使代码的理解复杂化。
- 对于小型优化,以类的classProperties形式或将useCallback用作函数的缓存处理程序函数-这样纯净的组件将不会不断呈现。 尤其是回调缓存可以派上用场,因此当将它们传递给PureComponent时,不会发生不必要的更新周期。
- 不要忘记,您在回调中没有收到真实事件,而是Syntetic事件。 如果退出当前功能,则将无法访问此事件的字段。 如果您有异步闭包,则缓存所需的字段。
第1部分。事件处理程序,缓存和代码感知
React提供了一种相当方便的方法来为html元素添加事件处理程序。
这是任何开发人员开始使用React编写时都应了解的基本知识之一:
class MyComponent extends Component { render() { return <button onClick={() => console.log('Hello world!')}>Click me</button>; } }
很简单? 从此代码中,可以立即清楚用户单击按钮时将发生什么。
但是,如果处理程序中的代码越来越多怎么办?
假设通过按钮,我们必须加载并过滤掉不在特定团队中的每个用户( user.team === 'search-team'
),然后按年龄对其进行排序。
class MyComponent extends Component { constructor(props) { super(props); this.state = { users: [] }; } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => { console.log('Hello world!'); window .fetch('/usersList') .then(result => result.json()) .then(data => { const users = data .filter(user => user.team === 'search-team') .sort((a, b) => { if (a.age > b.age) { return 1; } if (a.age < b.age) { return -1; } return 0; }); this.setState({ users: users, }); }); }} > Load users </button> </div> ); } }
此代码很难弄清楚。 业务逻辑代码与用户看到的布局混合在一起。
摆脱这种情况的最简单方法是将函数带到类方法的级别:
class MyComponent extends Component { fetchUsers() {
在这里,我们将业务逻辑从JSX代码移到了类中的单独字段中。 为了使它在函数内部可访问,我们以这种方式定义了回调: onClick={() => this.fetchUsers()}
另外,在描述类时,我们可以将字段声明为箭头函数:
class MyComponent extends Component { fetchUsers = () => {
这将使我们可以将回调声明为onClick={this.fetchUsers}
这两种方法有什么区别?
onClick={this.fetchUsers}
-在这里,每次在props中调用render函数时, button
将始终被发送相同的链接。
在onClick={() => this.fetchUsers()}
,每次调用render函数时,JavaScript都会初始化一个新函数() => this.fetchUsers()
并将其设置为onClick
prop。 这意味着在这种情况下, button
上的nextProp.onClick
和prop.onClick
将始终不相等,即使将组件标记为干净,也将对其进行渲染。
这对发展有什么威胁?
在大多数情况下,您不会从视觉上注意到性能下降,因为由组件生成的虚拟DOM与上一个虚拟DOM不会有所不同,并且DOM中不会有任何更改。
但是,如果呈现大量的组件或表列表,则会在大量数据上发现“刹车”。
为什么了解如何将函数转移到回调很重要?
通常在Twitter或stackoverflow上,您会遇到以下提示:
“如果React应用程序存在性能问题,请尝试用PureComponent替换Component的继承。另外,请记住,对于Component,您始终可以定义shouldComponentUpdate来摆脱不必要的更新循环。”
如果我们将组件定义为Pure,则意味着它已经具有应该在prop和nextProps之间执行shallowEqual的shouldComponentUpdate
函数。
每次将新的回调函数传递给此类组件,我们都会失去PureComponent
所有优势和优化。
让我们来看一个例子。
创建一个输入组件,该组件还将显示有关已更新多少次的信息:
class Input extends PureComponent { renderedCount = 0; render() { this.renderedCount++; return ( <div> <input onChange={this.props.onChange} /> <p>Input component was rerendered {this.renderedCount} times</p> </div> ); } }
让我们创建两个将在内部呈现Input的组件:
class A extends Component { state = { value: '' }; onChange = e => { this.setState({ value: e.target.value }); }; render() { return ( <div> <Input onChange={this.onChange} /> <p>The value is: {this.state.value} </p> </div> ); } }
第二个:
class B extends Component { state = { value: '' }; onChange(e) { this.setState({ value: e.target.value }); } render() { return ( <div> <Input onChange={e => this.onChange(e)} /> <p>The value is: {this.state.value} </p> </div> ); } }
您可以在这里用手尝试该示例: https : //codesandbox.io/s/2vwz6kjjkr
此示例说明了每次将新的回调函数传递给PureComponent时,如何失去PureComponent的所有优点。
第2部分。在函数组件中使用事件处理程序
在新版本的React(16.8)中,宣布了React钩子机制,使您可以编写功能完善的功能组件,并具有清晰的生命周期,可以涵盖几乎所有用户案例,而到目前为止,这些案例仅涉及类。
我们使用Input组件修改示例,以便所有组件都由一个函数表示并与React挂钩一起使用。
输入必须在其内部存储有关已更改次数的信息。 如果在类的情况下,我们在实例中使用了一个通过此字段实现访问的字段,那么在函数的情况下,我们将无法通过此字段声明变量。
React提供了一个useRef钩子,可用于在DOM树中保存对HtmlElement的引用,但也很有趣,因为它可用于组件所需的常规数据:
import React, { useRef } from 'react'; export default function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); }
我们还需要组件是“干净的”,也就是说,仅当传递给组件的道具已更改时才进行更新。
为此,有不同的库提供HOC,但是最好使用已经内置在React中的memo函数,因为它可以更快,更有效地工作:
import React, { useRef, memo } from 'react'; export default memo(function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); });
输入组件已准备就绪,现在我们重写组件A和B。
对于组件B,这很容易做到:
import React, { useState } from 'react'; function B() { const [value, setValue] = useState(''); return ( <div> <Input onChange={e => setValue(e.target.value)} /> <p>The value is: {value} </p> </div> ); }
在这里,我们使用了useState
钩子,如果组件由函数表示,则该钩子允许您保存和使用组件的状态。
我们如何缓存回调函数? 我们无法将其从组件中删除,因为在这种情况下,它对于组件的不同实例来说是常见的。
对于此类任务,React具有一组缓存和useCallback
挂钩,其中useCallback
最适合useCallback
https://reactjs.org/docs/hooks-reference.html
将此钩子添加到组件A
:
import React, { useState, useCallback } from 'react'; function A() { const [value, setValue] = useState(''); const onChange = useCallback(e => setValue(e.target.value), []); return ( <div> <Input onChange={onChange} /> <p>The value is: {value} </p> </div> ); }
我们缓存了该函数,这意味着Input组件不会每次都更新。
useCallback
挂钩如何工作?
这个钩子返回一个缓存的函数(即链接在渲染之间没有变化)。
除了要缓存的功能外,第二个参数也传递给它-一个空数组。
该数组允许您在更改需要传输功能的字段时传输字段列表,即 返回一个新链接。
useCallback
可以在此处查看将函数转移到回调的常规方法与useCallback
之间的区别: https : useCallback
为什么我们需要一个数组?
假设我们需要通过闭包来缓存依赖于某些值的函数:
import React, { useCallback } from 'react'; import ReactDOM from 'react-dom'; import './styles.css'; function App({ a, text }) { const onClick = useCallback(e => alert(a), [ ]); return <button onClick={onClick}>{text}</button>; } const rootElement = document.getElementById('root'); ReactDOM.render(<App text={'Click me'} a={1} />, rootElement);
在这里,App组件取决于prop a
。 如果您运行该示例,那么一切都会正常进行,直到我们添加到最后:
setTimeout(() => ReactDOM.render(<App text={'Next A'} a={2} />, rootElement), 5000);
触发超时后,单击警报中的按钮时,将显示1
。 发生这种情况是因为我们保存了前一个函数,该函数关闭a
变量。 并且由于a
是变量,在我们的例子中是值类型,并且值类型是不可变的,因此出现了此错误。 如果我们删除注释/*a*/
,那么代码将正常工作。 对第二个渲染进行反应将验证数组中传递的数据是否不同,并将返回一个新函数。
您可以在这里自己尝试以下示例: https : //codesandbox.io/s/6vo8jny1ln
React提供了许多允许您useRef
数据的功能,例如useRef
, useCallback
和useMemo
。
如果需要后者来useRef
函数值,并且它们与useRef
非常相似,则useRef
可以缓存对DOM元素的引用,还可以用作实例字段。
乍一看,它可以用于缓存功能,因为useRef
还在单独的组件更新之间缓存数据。
但是,不希望使用useRef
来缓存功能。 如果我们的函数使用闭包,那么在任何渲染中,闭包值都可以更改,并且我们的缓存函数可以使用旧值。 这意味着我们将需要编写函数更新逻辑或仅使用useCallback
,由于依赖机制,在其中实现了该函数。
https://codesandbox.io/s/p70pprpvvx在这里,您可以看到具有正确useCallback
,错误和useRef
。
第3部分。句法事件
我们已经弄清楚了如何使用事件处理程序以及如何正确使用回调中的闭包,但是在React中,使用它们时还有另一个非常重要的区别:
注意:现在,我们上面处理过的Input
绝对是同步的,但是在某些情况下,根据去抖动或节流模式,回调可能需要延迟进行。 因此,例如,反跳功能非常方便用于输入搜索字符串-仅在用户停止键入字符时才会进行搜索。
创建一个内部导致状态更改的组件:
function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); timerHandler.current = setTimeout(() => { setValue(e.target.value); }, 300);
此代码将不起作用。 事实是,React内部代理事件以及所谓的Syntetic Event进入了我们的onChange回调,该回调在我们的函数之后将被“清除”(字段将标记为null)。 出于性能原因,React这样做是为了使用单个对象,而不是每次都创建一个新对象。
如果像本例中那样需要取值,那么在退出函数之前就足以缓存必要的字段了:
function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); const pendingValue = e.target.value; // cached! timerHandler.current = setTimeout(() => { setValue(pendingValue); }, 300);
您可以在此处查看示例: https : //codesandbox.io/s/oj6p8opq0z
在极少数情况下,有必要维护事件的整个实例。 为此,您可以调用event.persist()
,它会删除
这是来自React事件的事件池的Syntetic事件的实例。
结论:
React事件处理程序非常方便,因为它们:
- 自动进行订阅和取消订阅(带有卸载组件);
- 简化了代码的理解,大多数订阅都易于在JSX代码中跟踪。
但是同时,在开发应用程序时,您可能会遇到一些困难:
- 覆盖props中的回调;
- 执行当前功能后清除的语法事件。
覆盖回调通常不会引起注意,因为vDOM不会改变,但是值得记住的是,如果您引入优化,通过从PureComponent
继承继承或使用memo
使用Pure替换组件,则应注意将其缓存,否则引入PureComponents或memo的好处将不明显。 对于缓存,可以使用classProperties(在使用类时)或useCallback
钩子(在使用函数时)。
为了进行正确的异步操作,如果您需要事件中的数据,还请缓存所需的字段。