Open jtwang7 opened 3 years ago
参考文章:
组件有时会共用多个类似的逻辑,我们通过拼接多个 HOC 来实现这一目的:
本例中,withData / withLogger / withLoading 为 HOC
const Loading = () => <p>loading</p>; withData( "https://jsonplaceholder.typicode.com/posts", { _limit: 10, page: 2 } )( withLogger('xxx')( withLoading(Loading)(Component) ) )
可以看到,多层的 HOC 嵌套形成的 HOC 嵌套地狱 会导致代码可阅读性很差,因此,我们需要实现一个 compose 组合函数进行优化。
compose 组合函数的本质是 pipe 管道函数。
pipe 管道函数的特点在于:利用 array.reduce()
方法,将上一次的函数结果作为下一次函数的参数进行递归。
而 compose 组合函数与 pipe 区别仅在于组合的方向不同:其利用 array.reduceRight()
方法从右到左对数组进行递归。
// pipe
const pipe = (...fns) => (initArg) => fns.reduce( ( prev, fn ) => fn(prev), initArg );
// compose
const compose = (...fns) => x => fns.reduceRight((x, fn) => fn(x), x);
pipe 和 compose 我们将其写为高阶函数的形式。第一组参数接受一个数组,包含所有要组合的函数,第二组参数接受第一个函数调用的初始参数。执行时利用 reduce 或者 reduceRight 进行递归,其内部回调返回函数的返回值作为下一个函数的参数。
从嵌套流角度考虑,我们首先会从最里层开始包裹原始的 WrappedComponent,然后将包裹后返回的新组件传给下一层 HOC 进行封装,因此最终包裹的组件如下:
reduceRight 能够让逻辑书写与实际包裹的形态匹配,更加直观。
// withData( withLogger( withLoading( WrappedComponent ) ) )
const compose = (...fns) => x => fns.reduceRight((x, fn) => fn(x), x); const fns = [withData, withLogger, withLoading ]; const cmp = compose( withData( "https://jsonplaceholder.typicode.com/posts", { _limit: 10, page: 2 } ), withLogger('xxx'), withLoading('loading'), );
cmp(WrappedComponent);
另一方面,我们也可以从这体会到 HOC 写成一个返回高阶组件的高阶函数所带来的优势:在 comopose 中,我们传入的是定制化后的 HOC,最终我们只需要再次调用函数,传入被包裹的组件,就能让 compose 函数内部按照 `Component => Component` 的方式执行,这也就是之前提到的:输入与输出类型相同时,更加容易组合。
React - 高阶组件 ( HOC )
参考文章:
前言
高阶组件(HOC)是一个函数,它接受一个组件作为参数然后返回一个新组件。 HOC 主要用于组件之间逻辑复用。比如你写了几个组件,他们之间的逻辑几乎相同,就可以用 HOC 对逻辑进行封装复用。
如何封装 HOC
参考 React 官方文档的 HOC 封装案例,主要注意以下几点:
但是,更合理的写法,应该将组件和额外参数分开写,如下所示:
将 withXXX 写成一个返回高阶组件的高阶函数!!! 这种形式可能看起来令人困惑或不必要,但它有一定的设计和理性: 我们可以先传入额外的参数,基于公共逻辑定制并返回一个高阶组件,该高阶组件仅包含单参数 WrappedComponent,因此其形式可表达为:
Component => Component
,输出类型与输入类型相同的函数很容易组合在一起,从而可实现高阶组件的嵌套组合。注意事项
不要在 render 方法中使用 HOC
React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。 若在 render 中使用 HOC,则在每次调用 render 函数都会创建一个新的 EnhancedComponent,这将导致子树每次渲染都会进行卸载,和重新挂载的操作!这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。
正确做法应该是在组件之外调用 HOC,并在组件内使用返回的新组件,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。一般来说,这跟你的预期表现是一致的。在极少数情况下,你需要动态调用 HOC。你可以在组件的生命周期方法或其构造函数中进行调用。 常见的两种 HOC 调用位置:
被包裹组件模块导出时:
调用组件的外部:
const HOCComponent = withHOC( ... )(WrappedComponent);
class MyComponent extends Component { render () { // ...
} }
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:
但要这样做,你需要知道哪些方法应该被拷贝。你可以使用
hoist-non-react-statics
自动拷贝所有非 React 静态方法:除了导出组件,另一个可行的方案是再额外导出这个静态方法。
Refs 不会被传递
虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。 这个问题的解决方案是通过使用 React.forwardRef API(React 16.3 中引入)。