React组件设计模式之-纯组件,函数组件,高阶组件

React
292
0
0
2023-01-08

一、组件

(1) 函数组件

如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
    {props.value}    </button>
  );
}

(2) React.Component

shouldComponentUpdate 仅检查了 props.color 或 state.count 是否改变。如果这些值没有改变,那么这个组件不会更新

class CounterButton extends React.Component {

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }
}

(3) PureComponent

如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent 就行了。

class CounterButton extends React.PureComponent {}

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较 (例如:1 == 1或者ture==true,数组和对象引用是否相同),所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。

不要在props和state中改变对象和数组,如果你在你的父组件中改变对象,你的PureComponent将不会更新。虽然值已经被改变,但是子组件比较的是之前props的引用是否相同,所以不会检测到不同。

因此,你可以通过使用es6的assign方法或者数组的扩展运算符或者使用第三方库,强制返回一个新的对象。

当数据结构很复杂时,情况会变得麻烦,存在性能问题

(比较原始值和对象引用是低耗时操作。如果你有一列子对象并且其中一个子对象更新,对它们的props和state进行检查要比重新渲染每一个子节点要快的多。)

(4) 何时使用Component 或 PureComponent ?

<1> 当组件是独立的,组件在页面中的个数为1或2的,组件有很多props、state,并且当中还有些是数组和对象的,组件需要每次都渲染的,使用Component

<2> 当组件经常作为子组件,作为列表,组件在页面中数量众多,组件props, state属性少,并且属性中基本没有数组和对象,组件不需要每次都渲染,只有变化了才渲染,使用PureComponent

凭主观,我觉得

以下组件适合Component

Button
Input

以下组件适合PureComponent

Radio
Checkbox
Option

二、高阶函数

HOC ( 高阶组件higherOrderComponent ) 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式

组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。(组件是 React 中代码复用的基本单元。)

高阶组件例如 Redux 的 connect 和 Relay 的 createFragmentContainer。

(1)HOC 不会修改传入的组件,也不会使用继承来复制其行为。

相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

(2)HOC 应该透传与自身无关的 props

HOC 为组件添加特性。自身不应该大幅改变约定。

HOC 应该透传与自身无关的 props,HOC 返回的组件与原组件应保持类似的接口。

(3)约定:包装显示名称以便轻松调试HOC

创建的容器组件会与任何其他组件一样,会显示在 React Developer Tools 中。为了方便调试,请选择一个显示名称,以表明它是 HOC 的产物

最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为 withSubscription,并且被包装组件的显示名称为 CommentList,显示名称应该为 WithSubscription(CommentList):

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

(4) 注意事项:

<1> 不要在 render 方法中使用 HOC

render() {
  // 每次调用 render 函数都会创建一个新的 EnhancedComponent 
  // EnhancedComponent1 !== EnhancedComponent2

  const EnhancedComponent = enhance(MyComponent);

  // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!

  return <EnhancedComponent />;
}

<2>务必复制静态方法

有时在 React 组件上定义静态方法很有用。例如,Relay 容器暴露了一个静态方法 getFragment 以方便组合 GraphQL 片段。

但是,当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。

// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:

你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

除了导出组件,另一个可行的方案是再额外导出这个静态方法。

// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';

<3> Refs 不会被传递

虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。

这个问题的解决方案是通过使用 React.forwardRef API(React 16.3 中引入)

三、React Redux 的 connect

React Redux 的 connect 函数是一个 返回高阶组件的高阶函数

最常见的 HOC 签名如下:

// React Redux 的 `connect` 函数
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

刚刚发生了什么?!如果你把它分开,就会更容易看出发生了什么。

// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(commentListSelector, commentListActions);

// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);

这种形式可能看起来令人困惑或不必要,但它有一个有用的属性。最大化可组合性

像 connect 函数返回的单参数 HOC 具有签名 Component => Component。 输出类型与输入类型相同的函数很容易组合在一起。参考 React面试题详细解答

// 而不是这样...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... 你可以编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))

const enhance = compose(
  // 这些都是单参数的 HOC
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

//(同样的属性也允许 connect 和其他 HOC 承担装饰器的角色)

四、其他

(1)key

每当一个列表重新渲染时,React 会根据每一项列表元素的 key 来检索上一次渲染时与每个 key 所匹配的列表项。如果 React 发现当前的列表有一个之前不存在的 key,那么就会创建出一个新的组件。如果 React 发现和之前对比少了一个 key,那么就会销毁之前对应的组件。如果一个组件的 key 发生了变化,这个组件会被销毁,然后使用新的 state 重新创建一份。

我们强烈推荐,每次只要你构建动态列表的时候,都要指定一个合适的 key。

如果你没有指定任何 key,React 会发出警告,并且会把数组的索引当作默认的 key。但是如果想要对列表进行重新排序、新增、删除操作时,把数组索引作为 key 是有问题的

显式地使用 key={i} 来指定 key 确实会消除警告,但是仍然和数组索引存在同样的问题,所以大多数情况下最好不要这么做。

组件的 key 值并不需要在全局都保证唯一,只需要在当前的同一级元素之前保证唯一即可。