React组件逻辑复用的那些事儿

2020-05-19 08:28 前端宇宙

基本每个开发者都需要考虑逻辑复用的问题,否则你的项目中将充斥着大量的重复代码。那么 React 是怎么复用组件逻辑的呢?本文将一一介绍 React 复用组件逻辑的几种方法,希望你读完之后能够有所收获。如果你对这些内容已经非常清楚,那么略过本文即可。

我已尽量对文中的代码和内容进行了校验,但是因为自身知识水平限制,难免有错误,欢迎在评论区指正。

1. Mixins

Mixins 事实上是 React.createClass 的产物了。当然,如果你曾经在低版本的 react 中使用过 Mixins,例如 react-timer-mixinreact-addons-pure-render-mixin,那么你可能知道,在 React 的新版本中我们其实还是可以使用 mixin,虽然 React.createClass 已经被移除了,但是仍然可以使用第三方库 create-react-class,来继续使用 mixin。甚至,ES6 写法的组件,也同样有方式去使用 mixin。当然啦,这不是本文讨论的重点,就不多做介绍了,如果你维护的老项目在升级的过程中遇到这类问题,可以与我探讨。

新的项目中基本不会出现 Mixins,但是如果你们公司还有一些老项目要维护,其中可能就应用了 Mixins,因此稍微花点时间,了解下 Mixins 的使用方法和原理,还是有必要的。倘若你完全没有这方面的需求,那么跳过本节亦是可以的。

Mixins 的使用

React 15.3.0 版本中增加了 PureComponent。而在此之前,或者如果你使用的是 React.createClass 的方式创建组件,那么想要同样的功能,就是使用 react-addons-pure-render-mixin,例如:

//下面代码在新版React中可正常运行,因为现在已经无法使用 `React.createClass`,我就不使用 `React.createClass` 来写了。

const createReactClass = require('create-react-class');
const PureRenderMixin = require('react-addons-pure-render-mixin');

const MyDialog = createReactClass({
    displayName'MyDialog',
    mixins: [PureRenderMixin],
    //other code
    render() {
        return (
            <div>
                {/* other code */}
            </div>

        )
    }
});

首先,需要注意,mixins 的值是一个数组,如果有多个 Mixins,那么只需要依次放在数组中即可,例如: mixins: [PureRenderMixin, TimerMixin]

Mixins 的原理

Mixins 的原理可以简单理解为将一个 mixin 对象上的方法增加到组件上。类似于 $.extend 方法,不过 React 还进行了一些其它的处理,例如:除了生命周期函数外,不同的 mixins 中是不允许有相同的属性的,并且也不能和组件中的属性和方法同名,否则会抛出异常。另外即使是生命周期函数,constructorrendershouldComponentUpdate 也是不允许重复的。

而如 compoentDidMount 的生命周期,会依次调用 Mixins,然后再调用组件中定义的 compoentDidMount

例如,上面的 PureRenderMixin 提供的对象中,有一个 shouldComponentUpdate 方法,即是将这个方法增加到了 MyDialog 上,此时 MyDialog 中不能再定义 shouldComponentUpdate,否则会抛出异常。

//react-addons-pure-render-mixin 源码
var shallowEqual = require('fbjs/lib/shallowEqual');

module.exports = {
  shouldComponentUpdatefunction(nextProps, nextState{
    return (
      !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState)
    );
  },
};

Mixins 的缺点

  1. Mixins 引入了隐式的依赖关系。

    例如,每个 mixin 依赖于其他的 mixin,那么修改其中一个就可能破坏另一个。

  2. Mixins 会导致名称冲突

    如果两个 mixin 中存在同名方法,就会抛出异常。另外,假设你引入了一个第三方的 mixin,该 mixin 上的方法和你组件的方法名发生冲突,你就不得不对方法进行重命名。

  3. Mixins 会导致越来越复杂

    mixin 开始的时候是简单的,但是随着时间的推移,容易变得越来越复杂。例如,一个组件需要一些状态来跟踪鼠标悬停,为了保持逻辑的可重用性,将 handleMouseEnter()handleMouseLeave()isHovering() 提取到 HoverMixin() 中。

    然后其他人可能需要实现一个提示框,他们不想复制 HoverMixin() 的逻辑,于是他们创建了一个使用 HoverMixinTooltipMixinTooltipMixin 在它的 componentDidUpdate 中读取 HoverMixin() 提供的 isHovering() 来决定显示或隐藏提示框。

    几个月之后,有人想将提示框的方向设置为可配置的。为了避免代码重复,他们将 getTooltipOptions() 方法增加到了 TooltipMixin 中。结果过了段时间,你需要在同一个组件中显示多个提示框,提示框不再是悬停时显示了,或者一些其他的功能,你需要解耦 HoverMixin()TooltipMixin 。另外,如果很多组件使用了某个 mixinmixin 中新增的功能都会被添加到所有组件中,事实上很多组件完全不需要这些新功能。

    渐渐地,封装的边界被侵蚀了,由于很难更改或移除现有的 mixin,它们变得越来越抽象,直到没有人理解它们是如何工作的。

React 官方认为在 React 代码库中,Mixin 是不必要的,也是有问题的。推荐开发者使用高阶组件来进行组件逻辑的复用。

2. HOC

React 官方文档对 HOC 进行了如下的定义:高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

简而言之,高阶组件就是一个函数,它接受一个组件为参数,返回一个新组件。

高阶组件形如下面这样:

//接受一个组件 WrappedComponent 作为参数,返回一个新组件 Proxy
function withXXX(WrappedComponent{
    return class Proxy extends React.Component {
        render() {
            return <WrappedComponent {...this.props}>
        }
    }
}

开发项目时,当你发现不同的组件有相似的逻辑,或者发现自己在写重复代码的时候,这时候就需要考虑组件复用的问题了。

这里我以一个实际开发的例子来说明,近期各大APP都在适配暗黑模式,而暗黑模式下的背景色、字体颜色等等和正常模式肯定是不一样的。那么就需要监听暗黑模式开启关闭事件,每个UI组件都需要根据当前的模式来设置样式。

每个组件都去监听事件变化来 setState 肯定是不可能的,因为会造成多次渲染。

这里我们需要借助 context API 来做,我以新的 Context API 为例。如果使用老的 context API 实现该功能,需要使用发布订阅模式来做,最后利用 react-native / react-dom 提供的 unstable_batchedUpdates 来统一更新,以避免多次渲染的问题(老的 context API 在值发生变化时,如果组件中 shouldComponentUpdate 返回了 false,那么它的子孙组件就不会重新渲染了)。

顺便多说一句,很多新的API出来的时候,不要急着在项目中使用,比如新的 Context API,如果你的 react 版本是 16.3.1, react-dom 版本是16.3.3,你会发现,当你的子组件是函数组件时,即是用 Context.Consumer 的形式时,你是能获取到 context 上的值,而你的组件是个类组件时,你根本拿不到 context 上的值。

同样的 React.forwardRef 在该版本食用时,某种情况下也有多次渲染的bug。都是血和泪的教训,不多说了,继续暗黑模式这个需求。

我的想法是将当前的模式(假设值为 light / dark)挂载到 context 上。其它组件直接从 context 上获取即可。不过我们知道的是,新版的 ContextAPI 函数组件和类组件,获取 context 的方法是不一致的。而且一个项目中有非常多的组件,每个组件都进行一次这样的操作,也是重复的工作量。于是,高阶组件就派上用场啦(PS:React16.8 版本中提供了 useContextHook,用起来很方便,不过我们当前的项目还无法使用)

当然,这里我使用高阶组件还有一个原因,就是我们的项目中还包含老的 context API (不要问我为什么不直接重构下,牵扯的人员太多了,没法随便改),新老 context API 在一个项目中是可以共存的,不过我们不能在同一个组件中同时使用。所以如果一个组件中已经使用的旧的 context API,要想从新的 context API 上获取值,也需要使用高阶组件来处理它。

于是,我编写了一个 withColorTheme 的高阶组件的雏形(这里可以认为 withColorTheme 是一个返回高阶组件的高阶函数):

import ThemeContext from './context';
function withColorTheme(options={}{
    return function(WrappedComponent{
        return class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {...this.propscolortheme={this.context}/>)
            }
        }
    }
}

包装显示名称

上面这个雏形存在几个问题,首先,我们没有为 ProxyComponent 包装显示名称,因此,为其加上:

import ThemeContext from './context';

function withColorTheme(options={}{
    function(WrappedComponent{
        class ProxyComponent extends React.Component {
            static contextType = ThemeContext;
            render() {
                return (<WrappedComponent {...this.propscolortheme={this.context}/>)
            }
        }
    }
    function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`;
    ProxyComponent.displayName = displayName;
    return ProxyComponent;
}

我们来看一下,不包装显示名称和包装显示名称的区别:

React Developer Tools 中调试

ReactNative的红屏报错

复制静态方法

众所周知,使用 HOC 包装组件,需要复制静态方法,如果你的 HOC 仅仅是某几个组件使用,没有静态方法需要拷贝,或者需要拷贝的静态方法是确定的,那么你手动处理一下也可以。

因为 withColorTheme 这个高阶组件,最终是要提供给很多业务使用的,无法限制别人的组件写法,因此这里我们必须将其写得通用一些。

hoist-non-react-statics 这个依赖可以帮助我们自动拷贝非 React 的静态方法,这里有一点需要注意,它只会帮助你拷贝非 React 的静态方法,而非被包装组件的所有静态方法。我第一次使用这个依赖的时候,没有仔细看,以为是将 WrappedComponent 上所有的静态方法都拷贝到 ProxyComponent。然后就遇到了 XXX.propsTypes.style undefined is not an object 的红屏报错(ReactNative调试)。因为我没有手动拷贝 propTypes,错误的以为 hoist-non-react-statics 会帮我处理了。

hoist-non-react-statics 的源码非常短,有兴趣的话,可以看一下,我当前使用的 3.3.2 版本。

本文章转载自公众号:gh_8184da923ced

首页 - 前端 相关的更多文章: