高阶函数
如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数。
// ES5
function isSearched(searchTerm) {
return function(item) {
return item.title.toLowerCase().includes(searchTerm.toLowerCase());
}
}
// ES6
const isSearched = searchTerm => item =>
item.title.toLowerCase().includes(searchTerm.toLowerCase());
React HOC
在实际项目中,组件总是趋于复杂,会包含很多逻辑。当发现在很多组件都需要处理相同逻辑的时候,就应该想办法抽象复用。在React中,具有复用功能的方式主要有:
- 公共组件库
- 公共方法库
- 类组件:
HOC
/Render Props
- 函数组件:
Hooks
当你没法把组件中的重复逻辑抽象成公共组件或者公共方法的情况下,在类组件中,就可以考虑引入HOC
。Render Props看这里。
和高阶函数类似,React HOC
就是把组件(也可增加一些可选参数)作为输入,然后输出一个新的组件,新组件内部处理一些通用逻辑,并使用输入的组件进行渲染。你可以将其视为参数化容器组件。
我们知道,在React中函数组件本质上就是一个函数。所以高阶函数组件就是一个
HOC
。
高阶组件应用
首先,我们先创建HOC
容器:
// 无状态函数组件
function HigherOrderComponent(WrappedComponent) {
return props => <WrappedComponent {...props} />;
}
// or
// 类组件
function HigherOrderComponent(WrappedComponent) {
return class extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// 类组件特有的通过继承的方式使用
// 也叫 反向继承(Inheritance Inversion)
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
};
}
通过继承输入组件形式实现的
HOC
又称为 反向继承(Inheritance Inversion)
以上HOC
只是一个容器,和直接使用 WrappedComponent
组件无二。那么,我们可以在HOC
中做点什么来达到共享复用的目的呢?主要通过以下三种方式:
- 操作参数(props / state)
- 条件渲染
- 组件包装
操作参数
我们可以通过HOC
对一个通用组件的 props
做一些操作达到复用的目的,而不用去修改这个通用组件。
举个简单的例子,一个列表展示组件,在新需求中的某些场景只希望展示列表中 status === '1'
的数据,那我们可以封装一个HOC
在这些场景使用,其他场景直接使用。
function HigherOrderComponent(WrappedComponent) {
return props => {
let newProps = props || []
if (newProps.length > 0){
newProps = newProps.filter((item) => item.status === '1')
}
return <WrappedComponent {...newProps} />
};
}
当然,你也可以通过修改这个通用组件来适配新场景。但是一直通过修改通用组件来适配所有场景,一方面存在兼容风险,另一方面适配逻辑太多,组件会变得臃肿,很难维护。
在举一个常见的应用:很多页面初始化的时候,都需要通过一个接口获取一些初始化数据。我们也可以通过HOC
封装,统一获取,然后作为扩展参数传递给页面组件。
function HigherOrderComponent(WrappedComponent) {
return props => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
return result.data;
};
const newProps = fetchData();
return <WrappedComponent {...props} {...newProps} />
};
}
条件渲染
在实际React
项目中,大多采用前端渲染模式。组件的渲染依赖Ajax
请求的数据。所以,我们经常会看到(我自己也写过)如下代码:
class BadComp extends React.Component {
state = {
isDateReady: false,
dataA: null,
dataB: null,
}
//...
render() {
if (!isDateReady) return false
const { isDateReady, hasDataA, hasDataB }= this.state
return (
<>
{ dataA && <A {...dataA}/> }
{ dataB && <B {...dataB}/> }
</>
);
}
};
上述代码并不是一无是处,至少是严谨的,规避了无数据或者数据异常导致页面奔溃的情况。但是上述组件有两个问题:
- 通过
isDateReady
阻断页面渲染,接口返回慢的话,白屏时间会很长。 - 页面充斥着太多类似
{ dataB && <B {...dataB}/> }
这种逻辑,不优雅,影响可读性。
解决上述问题,我们一般是通过增加 loading
和 占位
来优化。在页面主接口返回数据之前,增加 loading
效果。需要数据才能渲染的组件模块可以先显示 占位框
(css
),取得数据以后再重新渲染。很明显,这个解决方案比较通用,最好做成可复用的。这个时候,我们就可以通过HOC
的条件渲染来实现:
页面loading
效果的高阶组件,通过 反向继承 + 条件渲染 实现:
const withLoadingComponent = (WrappedComponent) => {
return (props) => {
render() {
if(this.state.isLoading) {
return <Loading />;
} else {
return super.render();
}
}
};
}
组件占位效果的HOC
。额外的 options
参数可以决定占位组件 Placeholder
的高度等信息。
const withPlaceholderComponent = (WrappedComponent, options) => {
return (props) => {
return props ? <WrappedComponent {...props}> : <Placeholder {...options}>
};
}
组件包装
如果你总是和相同的元素包裹使用一个通用组件,那么你可以通过抽象一个HOC
来复用。比如下面的HOC
在组件外面包裹一层背景色为 #fafafa
的 div
元素:
const withOtherComponent = (WrappedComponent) => {
return (props) => {
return (
<div style={{ backgroundColor: '#fafafa' }}>
<WrappedComponent {...this.props} {...newProps} />
</div>
)
};
}
这里只是举了一个简单的例子,实际项目中,也可以通过其他组件来包裹,或是组合使用。
Recompose 库
Recompose
是一个为函数式组件和高阶组件开发的 React
工具库。可以看作是 React
的 Lodash
。
复杂场景下,会存在多个 HOC
层层嵌套的情况:
const TodoListWithConditionalRendering = withLoadingIndicator(
withTodosNull(
withTodosEmpty(TodoList)
)
);
可以使用 recompose
库的 compost
方法优化:
import { compose } from 'recompose';
const withTodosNull = (Component) => (props) =>
...
const withTodosEmpty = (Component) => (props) =>
...
const withLoadingIndicator = (Component) => ({ isLoadingTodos, ...others }) =>
...
function TodoList({ todos }) {
...
}
const withConditionalRenderings = compose(
withLoadingIndicator,
withTodosNull,
withTodosEmpty
);
const TodoListWithConditionalRendering = withConditionalRenderings(TodoList);
function App(props) {
return (
<TodoListWithConditionalRendering
todos={props.todos}
isLoadingTodos={props.isLoadingTodos}
/>
);
}
compose
方法用于组合多个高阶组件。注意, props 流向是自上而下的。
另外,recompose
库的 pure
高阶组件,用于控制只在需要的时候重新呈现组件,即除非 props
发生了更改才重新重现组件。 withStateHandler
高阶组件,用于将组件状态和组件本身隔离开来……了解更多
Hoc问题
抽象地狱
也叫包装地狱。过多使用HOC
层层嵌套,会导致层级冗余,逻辑难追踪,很难维护。所以要避免Hoc滥用,充分考虑引入HOC
的必要性,尤其HOC
多层封装的情况。对于复杂的嵌套结构,最好增加充分的注释。
不要在render
中使用 HOC
。
每次render
渲染都会创建新的 HOC
。diff算法会对新旧子树进行 ====
比较。如果不相等,子树会进行卸载,和重新挂载的操作。而每次重新渲染创建的 HOC
前后是 !==
的。
render() {
// 每次调用 render 函数都会创建一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
return <EnhancedComponent />;
}
Refs
不会被传递。
虽然HOC
的约定是将所有 props
传递给被包装组件,但这对于 refs
并不适用。那是因为 ref
实际上并不是一个 prop
- 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。
这个问题的解决方案是通过使用 React.forwardRef
(React 16.3
中引入)。想要了解更多,可以阅览我的另一篇博文拥抱React-Hooks(二)-Refs
静态方法丢失。
因为原始组件被包裹在一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法。为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须准确知道应该拷贝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}