基本每一个开发者都须要考虑逻辑复用的问题,不然你的项目中将充斥着大量的重复代码。那么 React
是怎么复用组件逻辑的呢?本文将一一介绍 React
复用组件逻辑的几种方法,但愿你读完以后可以有所收获。若是你对这些内容已经很是清楚,那么略过本文便可。html
我已尽可能对文中的代码和内容进行了校验,可是由于自身知识水平限制,不免有错误,欢迎在评论区指正。react
Mixins
事实上是 React.createClass
的产物了。固然,若是你曾经在低版本的 react
中使用过 Mixins
,例如 react-timer-mixin
, react-addons-pure-render-mixin
,那么你可能知道,在 React
的新版本中咱们其实仍是可使用 mixin
,虽然 React.createClass
已经被移除了,可是仍然可使用第三方库 create-react-class
,来继续使用 mixin
。甚至,ES6 写法的组件,也一样有方式去使用 mixin
。固然啦,这不是本文讨论的重点,就很少作介绍了,若是你维护的老项目在升级的过程当中遇到这类问题,能够与我探讨。算法
新的项目中基本不会出现 Mixins
,可是若是大家公司还有一些老项目要维护,其中可能就应用了 Mixins
,所以稍微花点时间,了解下 Mixins
的使用方法和原理,仍是有必要的。假若你彻底没有这方面的需求,那么跳过本节亦是能够的。redux
React 15.3.0
版本中增长了 PureComponent
。而在此以前,或者若是你使用的是 React.createClass
的方式建立组件,那么想要一样的功能,就是使用 react-addons-pure-render-mixin
,例如:react-native
//下面代码在新版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
的原理能够简单理解为将一个 mixin
对象上的方法增长到组件上。相似于 $.extend
方法,不过 React
还进行了一些其它的处理,例如:除了生命周期函数外,不一样的 mixins
中是不容许有相同的属性的,而且也不能和组件中的属性和方法同名,不然会抛出异常。另外即便是生命周期函数,constructor
、render
和 shouldComponentUpdate
也是不容许重复的。数组
而如 compoentDidMount
的生命周期,会依次调用 Mixins
,而后再调用组件中定义的 compoentDidMount
。性能优化
例如,上面的 PureRenderMixin
提供的对象中,有一个 shouldComponentUpdate
方法,便是将这个方法增长到了 MyDialog
上,此时 MyDialog
中不能再定义 shouldComponentUpdate
,不然会抛出异常。app
//react-addons-pure-render-mixin 源码 var shallowEqual = require('fbjs/lib/shallowEqual'); module.exports = { shouldComponentUpdate: function(nextProps, nextState) { return ( !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) ); }, };
Mixins
引入了隐式的依赖关系。 例如,每一个 mixin
依赖于其余的 mixin
,那么修改其中一个就可能破坏另外一个。dom
Mixins
会致使名称冲突若是两个 mixin
中存在同名方法,就会抛出异常。另外,假设你引入了一个第三方的 mixin
,该 mixin
上的方法和你组件的方法名发生冲突,你就不得不对方法进行重命名。
Mixins
会致使愈来愈复杂mixin
开始的时候是简单的,可是随着时间的推移,容易变得愈来愈复杂。例如,一个组件须要一些状态来跟踪鼠标悬停,为了保持逻辑的可重用性,将 handleMouseEnter()
、handleMouseLeave()
和 isHovering()
提取到 HoverMixin()
中。
而后其余人可能须要实现一个提示框,他们不想复制 HoverMixin()
的逻辑,因而他们建立了一个使用 HoverMixin
的 TooltipMixin
,TooltipMixin
在它的 componentDidUpdate
中读取 HoverMixin()
提供的 isHovering()
来决定显示或隐藏提示框。
几个月以后,有人想将提示框的方向设置为可配置的。为了不代码重复,他们将 getTooltipOptions()
方法增长到了 TooltipMixin
中。结果过了段时间,你须要再同一个组件中显示多个提示框,提示框再也不是悬停时显示了,或者一些其余的功能,你须要解耦 HoverMixin()
和 TooltipMixin
。另外,若是不少组件使用了某个 mixin
,mixin
中新增的功能都会被添加到全部组件中,事实上不少组件彻底不须要这些新功能。
渐渐地,封装的边界被侵蚀了,因为很难更改或移除现有的mixin,它们变得愈来愈抽象,直到没有人理解它们是如何工做的。
React
官方认为在 React
代码库中,Mixin
是没必要要的,也是有问题的。推荐开发者使用高阶组件来进行组件逻辑的复用。
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 版本中提供了 useContext
的 Hook
,用起来很方便)
固然,这里我使用高阶组件还有一个缘由,就是咱们的项目中还包含老的 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.props} colortheme={this.context}/>) } } } }
上面这个雏形存在几个问题,首先,咱们没有为 ProxyComponent
包装显示名称,所以,为其加上:
import ThemeContext from './context'; function withColorTheme(options={}) { return function(WrappedComponent) { class ProxyComponent extends React.Component { static contextType = ThemeContext; render() { return (<WrappedComponent {...this.props} colortheme={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
版本。
所以,诸如 childContextTypes
、contextType
、contextTypes
、defaultProps
、displayName
、getDefaultProps
、getDerivedStateFromError
、getDerivedStateFromProps
mixins
、propTypes
、type
等不会被拷贝,其实也比较容易理解,由于 ProxyComponent
中可能也须要设置这些,不能简单去覆盖。
import ThemeContext from './context'; import hoistNonReactStatics from 'hoist-non-react-statics'; function withColorTheme(options={}) { return function(WrappedComponent) { class ProxyComponent extends React.Component { static contextType = ThemeContext; render() { return (<WrappedComponent {...this.props} colortheme={this.context}/>) } } } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`; ProxyComponent.displayName = displayName; ProxyComponent.WrappedComponent = WrappedComponent; ProxyComponent.propTypes = WrappedComponent.propTypes; //contextType contextTypes 和 childContextTypes 由于我这里不须要,就不拷贝了 return ProxyComponent; }
如今彷佛差很少了,不过呢,HOC
还有一个问题,就是 ref
传递的问题。若是不通过任何处理,咱们经过 ref
拿到的是 ProxyComponent
的实例,而不是本来想要获取的 WrappedComponent
的实例。
虽然咱们已经用无关的 props
进行了透传,可是 key
和 ref
不是普通的 prop
,React
会对它进行特别处理。
因此这里咱们须要对 ref
特别处理一下。若是你的 reac-dom
是 16.4.2
或者你的 react-native
版本是 0.59.9 以上,那么能够放心的使用 React.forwardRef
进行 ref
转发,这样使用起来也是最方便的。
使用 React.forwardRef 转发
import ThemeContext from './context'; import hoistNonReactStatics from 'hoist-non-react-statics'; function withColorTheme(options={}) { return function(WrappedComponent) { class ProxyComponent extends React.Component { static contextType = ThemeContext; render() { const { forwardRef, ...wrapperProps } = this.props; return <WrappedComponent {...wrapperProps} ref={forwardRef} colorTheme={ this.context } /> } } } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`; ProxyComponent.displayName = displayName; ProxyComponent.WrappedComponent = WrappedComponent; ProxyComponent.propTypes = WrappedComponent.propTypes; //contextType contextTypes 和 childContextTypes 由于我这里不须要,就不拷贝了 if (options.forwardRef) { let forwarded = React.forwardRef((props, ref) => ( <ProxyComponent {...props} forwardRef={ref} /> )); forwarded.displayName = displayName; forwarded.WrappedComponent = WrappedComponent; forwarded.propTypes = WrappedComponent.propTypes; return hoistNonReactStatics(forwarded, WrappedComponent); } else { return hoistNonReactStatics(ProxyComponent, WrappedComponent); } }
假设,咱们对 TextInput
进行了装饰,如 export default withColorTheme({forwardRef: true})(TextInput)
。
使用: <TextInput ref={v => this.textInput = v}>
若是要获取 WrappedComponent
的实例,直接经过 this.textInput
便可,和未使用 withColorTheme
装饰前同样获取。
经过方法调用
getWrappedInstance
import ThemeContext from './context'; import hoistNonReactStatics from 'hoist-non-react-statics'; function withColorTheme(options={}) { return function(WrappedComponent) { class ProxyComponent extends React.Component { static contextType = ThemeContext; getWrappedInstance = () => { if (options.forwardRef) { return this.wrappedInstance; } } setWrappedInstance = (ref) => { this.wrappedInstance = ref; } render() { const { forwardRef, ...wrapperProps } = this.props; let props = { ...this.props }; if (options.forwardRef) { props.ref = this.setWrappedInstance; } return <WrappedComponent {...props} colorTheme={ this.context } /> } } } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } const displayName = `WithColorTheme(${getDisplayName(WrappedComponent)})`; ProxyComponent.displayName = displayName; ProxyComponent.WrappedComponent = WrappedComponent; ProxyComponent.propTypes = WrappedComponent.propTypes; //contextType contextTypes 和 childContextTypes 由于我这里不须要,就不拷贝了 if (options.forwardRef) { let forwarded = React.forwardRef((props, ref) => ( <ProxyComponent {...props} forwardRef={ref} /> )); forwarded.displayName = displayName; forwarded.WrappedComponent = WrappedComponent; forwarded.propTypes = WrappedComponent.propTypes; return hoistNonReactStatics(forwarded, WrappedComponent); } else { return hoistNonReactStatics(ProxyComponent, WrappedComponent); } }
一样的,咱们对 TextInput
进行了装饰,如 export default withColorTheme({forwardRef: true})(TextInput)
。
使用: <TextInput ref={v => this.textInput = v}>
若是要获取 WrappedComponent
的实例,那么须要经过 this.textInput.getWrappedInstance()
获取被包装组件 TextInput
的实例。
我先说一下,为何我将它设计为下面这样:
function withColorTheme(options={}) { function(WrappedComponent) { } }
而不是像这样:
function withColorTheme(WrappedComponent, options={}) { }
主要是使用装饰器语法比较方便,并且不少业务中也使用了 react-redux
:
@connect(mapStateToProps, mapDispatchToProps) @withColorTheme() export default class TextInput extends Component { render() {} }
这样设计,能够不破坏本来的代码结构。不然的话,本来使用装饰器语法的业务改起来就有点麻烦。
回归到最大化可组合,看看官方文档怎么说:
像 connect
(react-redux
提供) 函数返回的单参数 HOC
具备签名 Component => Component
。输出类型与输入类型相同的函数很容易组合在一块儿。
// ... 你能够编写组合工具函数 // compose(f, g, h) 等同于 (...args) => f(g(h(...args))) const enhance = compose( // 这些都是单参数的 HOC withRouter, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)
compose
的源码能够看下 redux
的实现,代码很短。
再复杂化一下就是:
withRouter(connect(commentSelector)(withColorTheme(options)(WrappedComponent)));
咱们的 enhance
能够编写为:
const enhance = compose( withRouter, connect(commentSelector), withColorTheme(options) ) const EnhancedComponent = enhance(WrappedComponent)
若是咱们是写成 XXX(WrappedComponent, options)
的形式的话,那么上面的代码将变成:
const EnhancedComponent = withRouter(connect(withColorTheme(WrappedComponent, options), commentSelector))
试想一下,若是还有更多的 HOC
要使用,这个代码会变成什么样子?
约定
props
传递给被包裹的组件(HOC应透传与自身无关的 props
)注意事项
render
方法中使用 HOC
React
的 diff
算法(称为协调)使用组件标识来肯定它是应该更新现有子树仍是将其丢弃并挂载新子树。 若是从 render
返回的组件与前一个渲染中的组件相同(===
),则 React
经过将子树与新子树进行区分来递归更新子树。 若是它们不相等,则彻底卸载前一个子树。
这不只仅是性能问题 —— 从新挂载组件会致使该组件及其全部子组件的状态丢失。
若是在组件以外建立 HOC
,这样一来组件只会建立一次。所以,每次 render
时都会是同一个组件。
Refs
不会被传递(须要额外处理)React
官方文档上有这样一段描述: HOC
不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC
经过将组件包装在容器组件中来组成新组件。HOC
是纯函数,没有反作用。
所以呢,我以为反向继承不是 React
推崇的方式,这里咱们能够作一下了解,某些场景下也有可能会用到。
function withColor(WrappedComponent) { class ProxyComponent extends WrappedComponent { //注意 ProxyComponent 会覆盖 WrappedComponent 的同名函数,包括 state 和 props render() { //React.cloneElement(super.render(), { style: { color:'red' }}) return super.render(); } } return ProxyComponent; }
和上一节不一样,反向继承不会增长组件的层级,而且也不会有静态属性拷贝和 refs
丢失的问题。能够利用它来作渲染劫持,不过我目前没有什么必需要使用反向继承的场景。
虽然它没有静态属性和 refs
的问题,也不会增长层级,可是它也不是那么好用,会覆盖同名属性和方法这点就让人很无奈。另外虽然能够修改渲染结果,可是很差注入 props
。
首先, render props
是指一种在 React
组件之间使用一个值为函数的 prop
共享代码的简单技术。
具备 render prop
的组件接受一个函数,该函数返回一个 React
元素并调用它而不是实现本身的渲染逻辑。
<Route {...rest} render={routeProps => ( <FadeIn> <Component {...routeProps} /> </FadeIn> )} />
ReactNative
的开发者,其实 render props
的技术使用的不少,例如,FlatList
组件:
import React, {Component} from 'react'; import { FlatList, View, Text, TouchableHighlight } from 'react-native'; class MyList extends Component { data = [{ key: 1, title: 'Hello' }, { key: 2, title: 'World' }] render() { return ( <FlatList style={{marginTop: 60}} data={this.data} renderItem={({ item, index }) => { return ( <TouchableHighlight onPress={() => { alert(item.title) }} > <Text>{item.title}</Text> </TouchableHighlight> ) }} ListHeaderComponent={() => { return (<Text>如下是一个List</Text>) }} ListFooterComponent={() => { return <Text>没有更多数据</Text> }} /> ) } }
例如: FlatList
的 renderItem
、ListHeaderComponent
就是render prop
。
注意,render prop
是由于模式才被称为 render prop
,你不必定要用名为 render
的 prop
来使用这种模式。render prop
是一个用于告知组件须要渲染什么内容的函数 prop
。
其实,咱们在封装组件的时候,也常常会应用到这个技术,例如咱们封装一个轮播图组件,可是每一个页面的样式是不一致的,咱们能够提供一个基础样式,可是也要容许自定义,不然就没有通用价值了:
//提供一个 renderPage 的 prop class Swiper extends React.PureComponent { getPages() { if(typeof renderPage === 'function') { return this.props.renderPage(XX,XXX) } } render() { const pages = typeof renderPage === 'function' ? this.props.renderPage(XX,XXX) : XXXX; return ( <View> <Animated.View> {pages} </Animated.View> </View> ) } }
Render Props
和React.PureComponent
一块儿使用时要当心
若是在 render
方法里建立函数,那么 render props
,会抵消使用 React.PureComponent
带来的优点。由于浅比较 props 的时候总会获得 false
,而且在这种状况下每个 render
对于 render prop
将会生成一个新的值。
import React from 'react'; import { View } from 'react-native'; import Swiper from 'XXX'; class MySwiper extends React.Component { render() { return ( <Swiper renderPage={(pageDate, pageIndex) => { return ( <View></View> ) }} /> ) } }
这里应该比较好理解,这样写,renderPage
每次都会生成一个新的值,不少 React
性能优化上也会说起到这一点。咱们能够将 renderPage
的函数定义为实例方法,以下:
import React from 'react'; import { View } from 'react-native'; import Swiper from 'XXX'; class MySwiper extends React.Component { renderPage(pageDate, pageIndex) { return ( <View></View> ) } render() { return ( <Swiper renderPage={this.renderPage} /> ) } }
若是你没法静态定义 prop
,则 <Swiper>
应该扩展 React.Component
,由于也没有浅比较的必要了,就不要浪费时间去比较了。
Hook
是 React
16.8 的新增特性,它可让你在不编写 class
的状况下使用 state
以及其余的 React
特性。HOC
和 render props
虽然均可以
React
已经内置了一些 Hooks
,如: useState
、useEffect
、useContext
、useReducer
、useCallback
、useMemo
、useRef
等 Hook
,若是你还不清楚这些 Hook
,那么能够优先阅读一下官方文档。
咱们主要是将如何利用 Hooks
来进行组件逻辑复用。假设,咱们有这样一个需求,在开发环境下,每次渲染时,打印出组件的 props
。
import React, {useEffect} from 'react'; export default function useLogger(componentName,...params) { useEffect(() => { if(process.env.NODE_ENV === 'development') { console.log(componentName, ...params); } }); }
使用时:
import React, { useState } from 'react'; import useLogger from './useLogger'; export default function Counter(props) { let [count, setCount] = useState(0); useLogger('Counter', props); return ( <div> <button onClick={() => setCount(count + 1)}>+</button> <p>{`${props.title}, ${count}`}</p> </div> ) }
另外,官方文档自定义 Hook
章节也一步一步演示了如何利用 Hook
来进行逻辑复用。我由于版本限制,尚未在项目中应用 Hook
,虽然文档已经看过屡次。读到这里,通常都会有一个疑问,那就是 Hook
是否会替代 render props
和 HOC
,关于这一点,官方也给出了答案:
一般,render props
和高阶组件只渲染一个子节点。咱们认为让 Hook
来服务这个使用场景更加简单。这两种模式仍有用武之地,例如,FlatList
组件的 renderItem
等属性,或者是 一个可见的容器组件或许会有它本身的 DOM
结构。但在大部分场景下,Hook
足够了,而且可以帮助减小嵌套。
HOC
最最最讨厌的一点就是层级嵌套了,若是项目是基于新版本进行开发,那么须要逻辑复用时,优先考虑 Hook
,若是没法实现需求,那么再使用 render props
和 HOC
来解决。