个性化阅读
专注于IT技术分析

React教程:组件,挂钩和性能(2)

本文概述

正如我们的React教程的第一部分所指出的那样, React入门相对容易。首先使用Create React App(CRA), 启动一个新项目, 然后开始开发。遗憾的是, 随着时间的流逝, 你可能会遇到这样一种情况, 你的代码将变得很难维护, 尤其是在你不熟悉React的情况下。组件可能会不必要地变大, 或者最终可能包含可能是但不是的元素, 因此你最终可能在各处编写重复的代码。

在那儿, 你应该开始思考React开发解决方案, 从而真正开始你的React旅程。

每当你使用新应用程序时, 以后需要将新设计转换为React应用程序时, 请先尝试确定草图中将包含哪些组件, 如何分离草图以使其更易于管理以及哪些元素是重复(或至少是他们的行为)。尽量避免添加可能在”将来有用”的代码, 这可能很诱人, 但未来可能永远不会出现, 你将保留具有大量可配置选项的额外通用功能/组件。

React教程:React组件插图

另外, 如果某个组件的长度大于(例如2-3个窗口的高度), 则也许值得分开(如果可能), 因为以后阅读起来会更容易。

React中的受控组件与非受控组件

在大多数应用程序中, 需要输入以及与用户进行某种形式的交互, 以允许他们键入内容, 上载文件, 选择字段等。 React通过两种截然不同的方式处理用户交互, 即受控组件和不受控制组件。

顾名思义, 受控组件的值由React通过为与用户交互的元素提供值来控制, 而不受控制的元素不会获得value属性。因此, 我们只有一个真实的来源, 恰好是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中使用不受控制的组件的一种情况是文件类型输入, 因为它的值是只读的并且不能通过编程设置(需要用户交互)。此外, 我发现受控组件更易于阅读和使用。对控件的验证基于重新渲染, 可以更改状态, 并且我们可以轻松地指出输入内容有误(例如格式或为空)。

参考

我们已经提到了ref, ref是在类组件中可用的特殊功能, 直到在16.8中出现钩子为止。

引用可以使开发人员通过引用访问React组件或DOM元素(取决于我们附加引用的类型)。尝试避免使用它们并仅在必备方案中使用它们是一种好习惯, 因为它们会使代码难于读取并破坏自上而下的数据流。但是, 在某些情况下, 它们是必需的, 尤其是在DOM元素上(例如, 以编程方式更改焦点)。附加到React组件元素时, 你可以从所引用的组件中自由使用方法。尽管如此, 仍应避免这种做法, 因为有更好的方法来处理它(例如, 提升状态并将功能移至父组件)。

引用也有三种不同的实现方式:

  • 使用字符串文字(遗留, 应避免使用),
  • 使用在ref属性中设置的回调函数,
  • 通过将ref创建为React.createRef()并将其绑定到类属性并通过它进行访问(请注意, 在componentDidMount生命周期中可以使用引用)。

最后, 在某些情况下, ref不会被传递, 有时你想从当前组件访问更深的引用元素(例如, 你有一个<Button>组件, 该组件具有一个内部<input> DOM元素, 现在你是在<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; 
  }
}

高阶组件

React中经常提到高阶组件(HOC), 它是一种非常流行的模式, 你可能会使用(或已经这样做)。如果你熟悉HOC, 则可能已经在许多库中看到过导航, 连接和路由器。

HOC只是将组件作为参数的函数, 并且与没有HOC包装器的组件相比, 它将返回具有扩展功能的新组件。因此, 你可以实现一些易于扩展的功能, 这些功能可以增强你的组件(例如, 访问导航)。 HOC也可以采用几种形式, 具体取决于我们所拥有的, 唯一的参数始终需要是组件, 但是它可以采用额外的参数-一些选项, 或者像在connect中一样, 你首先调用具有配置的函数, 然后再返回一个函数它接受一个参数组件并返回HOC。

你可以添加以下几点, 应避免:

  • 为包装器HOC函数添加一个显示名称(因此, 通过更改HOC组件显示名称, 你实际上知道它是一个HOC)。
  • 不要在渲染方法中使用HOC, 你应该已经在其中使用了增强的组件, 而不是在其中创建新的HOC组件, 这是因为始终将其重新装入并丢失其当前状态。
  • 静态方法不会被复制, 因此, 如果要在新创建的HOC中包含一些静态方法, 则需要将它们复制到自己身上。
  • 提到的引用没有传递, 因此请使用前面提到的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样式有所不同。该字符串不会使用样式传递, 而是使用具有正确值的对象传递。样式属性也被驼峰化, 因此border-radius变为borderRadius, 依此类推。

React似乎已经普及了一些解决方案, 这些解决方案不仅在React中变得很普遍, 例如最近集成在CRA中的CSS模块, 你可以在其中简单地导入name.modules.css并使用其类(例如属性)来对组件进行样式设置(某些IDE (例如WebStorm)也具有”自动完成”功能, 可以告诉你可用的名称。

在React中也很流行的另一个解决方案是CSS-in-JS(例如, 情感库)。再次指出, CSS模块和情感(或一般来说是CSS-in-JS)不限于React。

React的钩子

自重写以来, 钩子很可能是React最热切期待的添加。产品是否符合宣传要求?在我看来, 是的, 因为它们确实是一个很棒的功能。它们本质上是打开新机会的功能, 例如:

  • 允许删除很多我们仅因无法使用的类组件而无法使用的类组件, 例如本地状态或引用, 因此该组件的代码看起来更易于阅读。
  • 使你可以使用更少的代码来达到相同的效果。
  • 使功能更易于思考和测试, 例如通过使用react-testing-library。
  • 也可以采用参数, 并且一个钩子的结果可以很容易地被另一个钩子使用(例如useEffect中useState的setState)。
  • 与类相比, Minins的方式更好, 对于Minifiers而言, 这往往会带来更多问题。
  • 可能会删除你的应用程序中的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());

最后, 如果我们要使用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有自己的道具检查功能。

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切换为”生产”, 并在Webpack的情况下添加UglifyJsPlugin。就CRA而言, 它就像使用npm run build(它将运行react-scripts build)一样简单。请注意, Webpack和CRA不是唯一的选择, 因为你可以使用其他构建工具, 例如Br​​unch。这通常包含在官方文档中, 无论是官方的React文档还是特定工具的文档。为了确保正确设置了模式, 你可以使用React Developer Tools, 这将向你指示你正在使用哪种构建(生产与开发)。上述步骤将使你的应用程序运行, 而不会受到来自React的检查和警告, 并且捆绑包本身也将被最小化。

你还可以为React应用程序做几件事。你如何处理内置的JS文件?如果大小相对较小, 则可以从” bundle.js”开始, 或者可以执行”供应商+捆绑包”之类的操作, 也可以执行”供应商+所需的最小零件+需要时导入的东西”。当你处理一个非常大的应用程序并且不需要一开始就导入所有内容时, 这很有用。请注意, 在主捆绑包中捆绑甚至没有使用的一些JavaScript代码只会增加捆绑包的大小, 并在一开始就使应用加载速度变慢。

如果你打算冻结库的版本, 并且意识到它们很长时间不会更改(如果有的话), 那么供应商捆绑包可能会很有用。此外, 较大的文件更适合gzip压缩, 因此有时从分离中获得的好处可能不值得。这取决于文件的大小, 有时你只需要自己尝试即可。

代码分割

代码拆分的出现方式可能比此处建议的要多, 但让我们集中讨论CRA和React本身提供的功能。基本上, 为了将代码分成不同的块, 我们可以使用import(), 这要感谢Webpack的支持(到目前为止, 导入本身是Stage 3中的建议, 因此它还不是语言标准的一部分)。每当Webpack看到导入时, 它将知道它需要在此阶段开始拆分代码, 并且不能将其包括在主包中(它位于导入内部)。

现在我们可以将其与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, 在本教程的第一部分中进行了说明)合作, 我们可以提高某些关键组件的性能。

显然, 对网络采用基本的最佳实践是最佳的, 例如对某些事件进行反跳(例如滚动), 对动画保持谨慎(使用变换而不是改变高度和对其进行动画处理)等等。使用最佳实践很容易被忽视, 特别是在你刚接触React的时候。

2019年及以后的React状态

如果我们要亲自讨论React的未来, 我不会太担心。在我看来, React将在2019年及以后保持其宝座。

在众多社区的支持下, React拥有如此强大的地位, 因此很难登基。 React社区很棒, 它还没有花光, 核心团队一直在努力改进React, 添加新功能并解决旧问题。 React也得到了一家大公司的支持, 但是许可问题却没有了-它现在是MIT许可的。

是的, 有些事情有望改变或改善;例如, 使React变小(提到的一种措施是删除合成事件)或将className重命名为class。当然, 即使这些看似很小的更改也可能导致诸如影响浏览器兼容性的问题。就我个人而言, 我还想知道当WebComponent获得更多普及时会发生什么, 因为它可能会增强React在当今经常使用的某些功能。我不认为它们将是彻底的替代, 但我相信它们可以很好地互补。

至于短期而言, React刚刚开始出现钩子。自从React重写之后, 这可能是最大的更改, 因为它们将打开很多可能性并增强其他功能组件(并且现在确实被大肆宣传)。

最后, 因为这是我最近所做的, 所以有React Native。对我来说, 这是一项伟大的技术, 在过去的几年中发生了很大的变化(缺少响应本机的链接可能是大多数人面临的最大问题, 而且显然存在很多错误)。 React Native正在对其核心进行重写, 并且应该以与React重写类似的方式来完成(它是内部的, 对于开发人员而言, 几乎没有任何更改)。异步渲染, 本机和JavaScript之间的更快, 更轻量的桥梁等等。

React生态系统中有很多值得期待的地方, 但是钩子更新(如果有人喜欢移动设备, 则为React Native)可能是我们在2019年将看到的最重要的变化。

相关:使用React Hooks重新验证时失效的数据:指南

赞(0)
未经允许不得转载:srcmini » React教程:组件,挂钩和性能(2)

评论 抢沙发

评论前必须登录!