React 教程:组件、钩子和性能
已发表: 2022-03-11正如我们在 React 教程的第一部分中所指出的,React 入门相对容易。 首先使用 Create React App (CRA),初始化一个新项目,然后开始开发。 可悲的是,随着时间的推移,您可能会遇到代码变得相当难以维护的情况,尤其是如果您是 React 新手。 组件可能会变得不必要地大,或者您最终可能会得到可能是组件但不是组件的元素,因此您最终可能会在这里和那里编写重复的代码。
这就是你应该尝试真正开始你的 React 旅程的地方——开始思考 React 开发解决方案。
每当你接近一个新的应用程序,一个你需要在以后转换成 React 应用程序的新设计时,首先尝试决定哪些组件将在你的草图中,你如何分离草图以使它们更易于管理,以及哪些元素是重复(或他们的行为,至少)。 尽量避免添加可能“在未来有用”的代码——这可能很诱人,但未来可能永远不会到来,您将保留具有大量可配置选项的额外通用功能/组件。
此外,如果一个组件的长度超过 2-3 个窗口高度,也许值得分开(如果可能的话)——因为以后更容易阅读。
React 中的受控组件与非受控组件
在大多数应用程序中,需要输入并与用户进行某种形式的交互,允许他们键入内容、上传文件、选择字段等。 React 以两种不同的方式处理用户交互——受控组件和不受控组件。
受控组件的值,顾名思义,是由 React 通过为与用户交互的元素提供值来控制的,而不受控的元素不会获得值属性。 多亏了这一点,我们有一个单一的事实来源,恰好是 React 状态,因此我们在屏幕上看到的内容与我们当前状态下的内容之间没有不匹配。 开发人员需要传递一个函数来响应用户与表单的交互,这将改变其状态。
class ControlledInput extends React.Component { state = { value: "" }; onChange = (e) => this.setState({ value: e.target.value }); render() { return ( <input value={this.state.value} onChange={this.onChange}/> ); } }
在不受控制的 React 组件中,我们不关心值如何变化,但如果我们想知道确切的值,我们只需通过 ref 访问它。
class UncontrolledInput extends React.Component { input = React.createRef(); getValue = () => { console.log(this.input.current.value); }; render() { return ( <input ref={this.input}/> ); } }
那么什么时候应该使用呢? 我会说受控组件在大多数情况下都是可行的方法,但也有一些例外。 例如,您需要在 React 中使用不受控制的组件的一种情况是file
类型输入,因为它的值是只读的并且不能以编程方式设置(需要用户交互)。 此外,我发现受控组件更易于阅读和使用。 对受控组件进行验证是基于重新渲染,状态可以改变,我们可以很容易地指出输入有问题(例如,格式或为空)。
参考文献
我们已经提到了refs
,这是一个在类组件中可用的特殊功能,直到钩子出现在 16.8 中。
Refs 可以让开发人员通过引用访问 React 组件或 DOM 元素(取决于我们附加 ref 的类型)。 尝试避免使用它们并仅在必备场景中使用它们被认为是一种好习惯,因为它们会使代码更难阅读并破坏自上而下的数据流。 然而,在某些情况下它们是必要的,尤其是在 DOM 元素上(例如,以编程方式改变焦点)。 附加到 React 组件元素时,您可以自由地使用您所引用的该组件中的方法。 尽管如此,应该避免这种做法,因为有更好的方法来处理它(例如,提升状态并将函数移动到父组件)。
Refs 也有三种不同的实现方式:
- 使用字符串文字(遗留,应该避免),
- 使用在 ref 属性中设置的回调函数,
- 通过将 ref 创建为
React.createRef()
并将其绑定到类属性并通过它访问它(请注意,引用将在 componentDidMount 生命周期中可用)。
最后,在某些情况下,没有传递 refs,并且有时您想从当前组件访问更深层次的引用元素(例如,您有一个具有内部<input>
DOM 元素的<Button>
组件,而现在您位于<Row>
组件中,并且从您想要访问的行组件中输入 DOM 焦点功能。这就是您将使用forwardRef
的地方)。
没有传递引用的一种情况是在组件上使用了更高阶的组件 - 原因很容易理解,因为ref
不是prop
(类似于key
)所以它没有被传递,所以它会引用HOC
而不是被它包装的组件。 在这种情况下,我们可以使用React.forwardRef
,它将 props 和 refs 作为参数,然后可以将其分配给prop
并传递给我们想要访问的组件。
function withNewReference(Component) { class Hoc extends React.Component { render() { const {forwardedRef, ...props} = this.props; return <Component ref={forwardedRef} {...props}/>; } } return React.forwardRef((props, ref) => { return <Hoc {...props} forwardedRef={ref} />; }); }
误差边界
事情越复杂,出错的可能性就越高。 这就是为什么错误边界是 React 的一部分。 那么它们是如何工作的呢?
如果出现问题并且没有错误边界作为其父级,它将导致整个 React 应用程序失败。 最好不要显示信息,而不是误导用户显示错误信息,但这并不一定意味着你应该让整个应用程序崩溃并显示白屏。 使用错误边界,您可以使用更大程度的灵活性。 您可以在整个应用程序中使用一个并显示错误消息,或者在某些小部件中使用它而不显示它们,或者显示少量信息来代替这些小部件。
请记住,这只是关于声明性代码的问题,而不是您为处理某些事件或调用而编写的命令性代码。 对于这些,您仍应使用常规的try/catch方法。
错误边界也是您可以将信息发送到您使用的错误记录器的地方(在componentDidCatch
生命周期方法中)。
class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, info) { logToErrorLogger(error, info); } render() { if (this.state.hasError) { return <div>Help, something went wrong.</div>; } return this.props.children; } }
高阶组件
高阶组件 (HOC)在 React 中经常被提及,并且是一种非常流行的模式,您可能会使用(或已经使用过)。 如果您熟悉 HOC,您可能已经在许多库中看到withNavigation, connect, withRouter
。
HOC 只是将组件作为参数的函数,与没有 HOC 包装器的组件相比,它将返回具有扩展功能的新组件。 多亏了这一点,您可以实现一些易于扩展的功能,这些功能可以增强您的组件(例如,访问导航)。 HOC 也可以根据我们拥有的内容采用几种调用形式,唯一的参数总是需要是一个组件,但它可以采用额外的参数——一些选项,或者像在connect
中一样,你首先调用一个带有配置的函数,然后返回一个函数它接受一个参数组件并返回 HOC。
您可以添加并应避免以下几点:
- 为您的包装器 HOC 函数添加一个显示名称(通过更改您的 HOC 组件显示名称,您知道它实际上是一个 HOC)。
- 不要在渲染方法中使用 HOC——你应该已经在其中使用了一个增强的组件,而不是在那里创建一个新的 HOC 组件,因为它一直在重新安装并丢失其当前状态。
- 静态方法不会被复制过来,所以如果你想在新创建的 HOC 中包含一些静态方法,你需要自己复制它们。
- 提到的Refs没有通过,所以使用前面提到的
React.forwardRef
来解决此类问题。
export function importantHoc() { return (Component) => class extends React.Component { importantFunction = () => { console.log("Very Important Function"); }; render() { return ( <Component {...this.props} importantFunction={this.importantFunction} /> ); } }; }
造型
样式不一定与 React 本身有关,但由于多种原因值得一提。
首先,常规的 CSS/内联样式在这里正常应用,您只需在 className 属性中添加 CSS 中的类名,它就会正常工作。 内联样式与常规的 HTML 样式有点不同。 该字符串不是与样式一起传递的,而是与每个具有正确值的对象一起传递的。 样式属性也是camelCased,所以border-radius变成borderRadius等等。
React 似乎已经普及了一些不仅在 React 中变得司空见惯的解决方案,例如最近集成到 CRA 中的 CSS 模块,您可以在其中简单地导入name.modules.css
并使用它的类(如属性)来设置组件的样式(一些 IDE ,例如,WebStorm,也有自动完成功能,它会告诉您可用的名称)。
在 React 中也很流行的另一个解决方案是 CSS-in-JS(例如, emotion
库)。 再次指出,CSS 模块和情感(或一般的 CSS-in-JS)不限于 React。
React 中的钩子
自重写以来, Hooks很可能是 React 中最受期待的新增功能。 产品是否辜负了炒作? 从我的角度来看,是的,因为它们确实是一个很棒的功能。 它们本质上是开辟新机会的功能,例如:
- 允许删除许多我们只使用因为我们不能拥有的
class
组件,例如本地状态或引用,因此组件的代码看起来更容易阅读。 - 使您能够使用更少的代码来获得相同的效果。
- 使函数更容易思考和测试,例如,通过使用 react-testing-library。
- 也可以带参数,一个的结果可以很容易地被另一个钩子使用(例如,
setState
fromuseState
inuseEffect
)。 - 比类更好地缩小方法,这对于缩小器来说往往有点问题。
- 可能会在您的应用程序中删除 HOC 和渲染道具模式,尽管这些模式是为了解决其他问题而设计的。
- 能够由任何熟练的 React 开发人员定制。
默认包含的 React 钩子很少。 三个基本的是useState
、 useEffect
和useContext
。 还有几个附加的,例如useRef
和useMemo
,但现在,我们将专注于基础知识。
让我们看一下useState
,让我们用它来创建一个简单的计数器示例。 它是如何工作的? 好吧,基本上,整个结构非常简单,看起来像:
export function Counter() { const [counter, setCounter] = React.useState(0); return ( <div> {counter} <button onClick={() => setCounter(counter + 1)}>+</button> </div> ); };
它使用initialState
(value) 调用并返回一个包含两个元素的数组。 由于数组解构分配,我们可以立即将变量分配给这些元素。 第一个总是更新后的最后一个状态,而另一个是我们将用来更新值的函数。 看起来相当容易,不是吗?
此外,由于此类组件过去被称为无状态功能组件,因此此类名称不再合适,因为它们可以具有如上所示的状态。 因此,类组件和函数组件的名称似乎更符合它们的实际用途,至少从 16.8.0 开始。
更新函数(在我们的例子中setCounter
)也可以用作一个函数,它将先前的值作为以下形式的参数:
<button onClick={() => setCounter(prevCounter => prevCounter + 1)}>+</button> <button onClick={() => setCounter(prevCounter => prevCounter - 1)}>-</button>
然而,与执行浅合并的this.setState
类组件不同,设置函数(在我们的例子中为setCounter
)是覆盖整个状态。

此外, initialState
也可以是一个函数,而不仅仅是一个普通的值。 这有其自身的好处,因为该函数将仅在组件的初始渲染期间运行,之后将不再调用它。
const [counter, setCounter] = useState(() => calculateComplexInitialValue());
最后,如果我们要使用与当前状态( counter
)相同时刻完全相同的值的setCounter
,那么组件将不会重新渲染。
另一方面, useEffect
是为我们的功能组件添加副作用,无论是订阅、API 调用、计时器,还是我们可能认为有用的任何东西。 我们将传递给useEffect
的任何函数都将在渲染后运行,并且它将在每次渲染后运行,除非我们添加关于哪些属性的更改应作为函数的第二个参数重新运行的限制。 如果我们只想在挂载时运行它并在卸载时清理,那么我们只需要在其中传递一个空数组。
const fetchApi = async () => { const value = await fetch("https://jsonplaceholder.typicode.com/todos/1"); console.log(await value.json()); }; export function Counter() { const [counter, setCounter] = useState(0); useEffect(() => { fetchApi(); }, []); return ( <div> {counter} <button onClick={() => setCounter(prevCounter => prevCounter + 1)}>+</button> <button onClick={() => setCounter(prevCounter => prevCounter - 1)}>-</button> </div> ); };
由于第二个参数为空数组,上述代码将只运行一次。 基本上,在这种情况下,它类似于componentDidMount
,但它会稍后触发。 如果您想在浏览器绘制之前调用类似的钩子,请使用useLayoutEffect
,但这些更新将同步应用,与useEffect
不同。
useContext
似乎是最容易理解的,因为它提供了我们想要访问的上下文(由createContext
函数返回的对象),作为回报,它为我们提供了该上下文的值。
const context = useContext(Context);
最后,要编写自己的钩子,您可以编写如下内容:
function useWindowWidth() { let [windowWidth, setWindowWidth] = useState(window.innerWidth); function handleResize() { setWindowWidth(window.innerWidth); } useEffect(() => { window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return windowWidth; }
基本上,我们使用的是常规的useState
钩子,我们将其分配为初始值窗口宽度。 然后在useEffect,
我们添加了一个侦听器,它将在每次调整窗口大小时触发handleResize
。 我们还清除了组件将被卸载后(查看useEffect
中的返回)。 简单?
注意:所有钩子中的使用这个词很重要。 之所以使用它是因为它允许 React 检查你是否在做坏事,例如,从常规 JS 函数调用钩子。
检查类型
在 Flow 和 TypeScript 成为一个选项之前,React 有自己的 props 检查。
PropTypes 检查 React 组件接收到的属性(props)是否与我们所拥有的一致。 每当发生不同的情况(例如,对象而不是数组)时,我们都会在控制台中收到警告。 需要注意的是,PropTypes 仅在开发模式下检查,因为它们对性能的影响和前面提到的控制台警告。
从 React 15.5 开始,PropTypes 位于不同的包中,需要单独安装。 它们在名为propTypes
的静态属性(惊喜)中与属性一起声明,将它们与defaultProps
结合使用,如果属性未定义(唯一的情况是未定义),则使用 defaultProps。 DefaultProps 与 PropTypes 无关,但它们可以解决一些由于 PropTypes 可能出现的警告。
另外两个选项是 Flow 和 TypeScript,它们现在更流行(尤其是 TypeScript)。
- TypeScript是由 Microsoft 开发的 JavaScript 的类型化超集,它甚至可以在应用程序运行之前检查错误,并为开发提供卓越的自动完成功能。 它还极大地改进了重构。 由于 Microsoft 支持,它在类型语言方面拥有丰富的经验,因此它也是一个相当安全的选择。
- 与 TypeScript 不同, Flow不是一种语言。 它是 JavaScript 的静态类型检查器,因此它更像是包含在 JavaScript 中的工具,而不是一种语言。 Flow 背后的整个想法与 TypeScript 提供的非常相似。 它允许您添加类型,因此在运行代码之前不太可能出现任何错误。 就像 TypeScript 一样,现在 CRA(创建 React 应用程序)从一开始就支持 Flow。
就个人而言,我发现 TypeScript 更快(几乎是瞬时的),尤其是在自动完成方面,Flow 似乎有点慢。 值得注意的是,我个人使用的 WebStorm 等 IDE 使用 CLI 与 Flow 集成。 但是,在文件中集成可选使用似乎更容易,您只需在文件开头添加// @flow
即可开始类型检查。 此外,据我所知,TypeScript 似乎最终赢得了与 Flow 的战斗——它现在更流行了,一些最受欢迎的库正在从 Flow 重构为 TypeScript。
官方文档中也提到了更多选项,例如 Reason(由 Facebook 开发并在 React 社区中广受欢迎)、Kotlin(由 JetBrains 开发的语言)等等。
显然,对于前端开发人员来说,最简单的方法是加入并开始使用 Flow 和 TypeScript,而不是切换到 Kotlin 或 F#。 但是,对于正在过渡到前端的后端开发人员来说,这些实际上可能更容易上手。
生产和反应性能
对于生产模式,您需要做的最基本和最明显的更改是为DefinePlugin
切换到“生产”,并在UglifyJsPlugin
的情况下添加 UglifyJsPlugin。 在 CRA 的情况下,它就像使用npm run build
一样简单(它将运行react-scripts build
)。 请注意,Webpack 和 CRA 不是唯一的选择,因为您可以使用其他构建工具,例如 Brunch。 这通常包含在官方文档中,无论是官方 React 文档还是特定工具的文档。 为确保模式设置正确,您可以使用 React 开发者工具,它会指示您正在使用哪种构建(生产与开发)。 上述步骤将使您的应用程序在没有来自 React 的检查和警告的情况下运行,并且捆绑包本身也将被最小化。
你可以为你的 React 应用做更多的事情。 你如何处理构建的 JS 文件? 如果大小相对较小,您可以只从“bundle.js”开始,或者可以执行“供应商 + 捆绑”或“供应商 + 最小所需部分 + 在需要时导入东西”之类的操作。 当您处理一个非常大的应用程序并且您不需要在一开始就导入所有内容时,这很有用。 请注意,在主包中捆绑一些甚至不使用的 JavaScript 代码只会增加包的大小,并使应用程序在一开始时加载速度变慢。
如果您计划在意识到库的版本可能很长时间不会更改(如果有的话)后冻结它们,那么供应商捆绑包可能会很有用。 此外,更大的文件更适合 gzip,因此您从分离中获得的好处有时可能不值得。 这取决于文件大小,有时您只需要自己尝试一下。
代码拆分
代码拆分的方式可能比这里建议的更多,但让我们关注我们在 CRA 和 React 本身中可用的内容。 基本上,为了将代码分成不同的块,我们可以使用import()
,这要归功于 Webpack ( import
本身是目前第 3 阶段的提议,因此它还不是语言标准的一部分)。 每当 Webpack 看到import
,它就会知道它需要在这个阶段开始代码拆分,并且不能将它包含在 main bundle 中(它在 import 中的代码)。
现在我们可以将它与React.lazy()
连接起来,这需要import()
的文件路径包含需要在该位置呈现的组件。 接下来,我们可以使用React.suspense()
它将在该位置显示不同的组件,直到加载导入的组件。 有人可能想知道; 如果我们要导入单个组件,那我们为什么需要它?
情况并非如此,因为React.lazy()
将显示我们import()
的组件,但import()
可能会获取比单个组件更大的块。 例如,该特定组件可能包含其他库、更多代码等,因此不需要一个文件——它可能是捆绑在一起的更多文件。 最后,我们可以将所有这些包装在ErrorBoundary
中(您可以在我们关于错误边界的部分中找到代码) ,如果我们想要导入的组件出现故障(例如,如果存在网络错误),它将作为后备。
import ErrorBoundary from './ErrorBoundary'; const ComponentOne = React.lazy(() => import('./ComponentOne')); function MyComponent() { return ( <ErrorBoundary> <React.Suspense fallback={<div>Loading...</div>}> <ComponentOne/> </React.Suspense> </ErrorBoundary> ); }
这是一个基本示例,但您显然可以做得更多。 您可以使用import
和React.lazy
进行动态路由拆分(例如,管理员与普通用户,或者只是带来很多东西的非常大的路径)。 请注意, React.lazy
仅支持默认导出,不支持服务器端渲染。
反应代码性能
关于性能,如果您的 React 应用程序运行缓慢,有两个工具可以帮助您找出问题所在。
第一个是 Chrome 性能选项卡,它将告诉您每个组件发生了什么(例如,挂载、更新)。 多亏了这一点,您应该能够确定哪个组件出现性能问题,然后对其进行优化。
另一种选择是使用在 React 16.5+ 中可用的 DevTools Profiler,在 shouldComponentUpdate(或 PureComponent,在本教程的第一部分中解释)的合作下,我们可以提高一些关键组件的性能。
显然,对 web 使用基本的最佳实践是最佳的,例如去抖动一些事件(例如,滚动),对动画保持谨慎(使用变换而不是改变高度并为其设置动画)等等。 使用最佳实践很容易被忽视,尤其是当你刚刚开始接触 React 时。
2019 年及以后的 React 状态
如果我们要讨论 React 的未来,就个人而言,我不会太担心。 从我的角度来看,React 在 2019 年及以后保持其宝座将毫无困难。
React 拥有如此强大的地位,并得到大量社区的支持,以至于它很难被淘汰。 React 社区很棒,它并没有枯竭的想法,核心团队一直在努力改进 React,添加新功能并修复旧问题。 React 也得到了一家大公司的支持,但是许可问题已经解决了——它现在是 MIT 许可的。
是的,有一些事情需要改变或改进; 例如,使 React 更小一些(提到的措施之一是删除合成事件)或将className
重命名为class.
当然,即使是这些看似微小的更改也可能导致影响浏览器兼容性等问题。 就个人而言,我也想知道当 WebComponent 越来越受欢迎时会发生什么,因为它可能会增加 React 今天经常使用的一些东西。 我不相信它们会完全替代,但我相信它们可以很好地互补。
至于短期,钩子刚刚来到 React。 这可能是自 React 重写以来最大的变化,因为它们将开辟大量可能性并增强更多功能组件(而且它们现在真的被大肆宣传了)。
最后,正如我最近一直在做的那样,还有 React Native。 对我来说,这是一项伟大的技术,在过去的几年中发生了如此大的变化(缺乏 react-native 链接可能是大多数人最大的问题,而且显然存在大量错误)。 React Native 正在重写其核心,这应该以与 React 重写类似的方式完成(这都是内部的,对于开发人员来说,什么都不应该或几乎什么都不应该改变)。 异步渲染、原生和 JavaScript 之间更快、更轻量级的桥梁等等。
React 生态系统有很多值得期待的地方,但 hooks(如果有人喜欢手机,还有 React Native)更新可能是我们将在 2019 年看到的最重要的变化。