强化 props 就是在原始组件的 props 基础上,加入一些其他的 props ,强化原始组件功能。举个例子,为了让组件也可以获取到路由对象,进行路由跳转等操作,所以 React Router 提供了类似 withRouter 的 HOC 。
注:withRouter 是 React Router v5 提供的 HOC,v6 起已被移除(改用 useNavigate / useLocation 等 hooks),下面的源码仅用于讲解 HOC 的编写思路。
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
/* 获取 */
const { wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{context => {
return (
<Component
{...remainingProps} // 组件原始的props
{...context} // 存在路由对象的上下文,history location 等
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
/* 继承静态属性 */
return hoistStatics(C, Component);
}
export default withRouter流程分析:
HOC 反向继承模式,可以通过 super.render() 得到 render 之后的内容,利用这一点,可以做渲染劫持 ,更有甚者可以修改 render 之后的 React element 对象。
const HOC = (WrapComponent) =>
class Index extends WrapComponent {
render() {
if (this.props.visible) {
return super.render()
} else {
return <div>暂无数据</div>
}
}
}
修改渲染树
class Index extends React.Component{
render(){
return <div>
<ul>
<li>react</li>
<li>vue</li>
<li>Angular</li>
</ul>
</div>
}
}
function HOC (Component){
return class Advance extends Component {
render() {
const element = super.render()
const otherProps = {
name:'alien'
}
/* 替换 Angular 元素节点 */
const appendElement = React.createElement('li' ,{} , `hello ,world , my name is ${ otherProps.name }` )
const newchild = React.Children.map(element.props.children.props.children,(child,index)=>{
if(index === 2) return appendElement
return child
})
return React.cloneElement(element, element.props, newchild)
}
}
}
export default HOC(Index)dva 中 dynamic 就是配合 import ,实现组件的动态加载的,而且每次切换路由,都会有 Loading 效果,接下来看看大致的实现思路。
编写
export default function dynamicHoc(loadRouter) {
return class Content extends React.Component {
state = {Component: null}
componentDidMount() {
if (this.state.Component) return
loadRouter()
.then(module => module.default) // 动态加载 component 组件
.then(Component => this.setState({Component},
))
}
render() {
const {Component} = this.state
return Component ? <Component {
...this.props
}
/> : <Loading />
}
}
}使用
const Index = AsyncRouter(()=>import('../pages/index'))实现思路:
对于属性代理虽然不能直接获取组件内的状态,但是可以通过 ref 获取组件实例,获取到组件实例,就可以获取组件的一些状态,或是手动触发一些事件,进一步强化组件,但是注意的是:类组件才存在实例,函数组件不存在实例。
function Hoc(Component){
return class WrapComponent extends React.Component{
constructor(){
super()
this.node = null /* 获取实例,可以做一些其他的操作。 */
}
render(){
return <Component {...this.props} ref={(node) => this.node = node } />
}
}
}HOC 不一定非要对组件本身做些什么?也可以单纯增加一些事件监听,错误监控。接下来做一个 HOC ,只对组件内的点击事件做一个监听效果。
function ClickHoc (Component){
return function Wrap(props){
const dom = useRef(null)
useEffect(()=>{
const handerClick = () => console.log('发生点击事件')
dom.current.addEventListener('click',handerClick)
return () => dom.current.removeEventListener('click',handerClick)
},[])
return <div ref={dom} ><Component {...props} /></div>
}
}
@ClickHoc
class Index extends React.Component{
render(){
return <div className='index' >
<p>hello,world</p>
<button>组件内部点击</button>
</div>
}
}
export default ()=>{
return <div className='box' >
<Index />
<button>组件外部点击</button>
</div>
}下面对 HOC 具体能实现那些功能,和如何编写做一下总结:
function HOC (Component){
const proDidMount = Component.prototype.componentDidMount
Component.prototype.componentDidMount = function(){
console.log('劫持生命周期:componentDidMount')
proDidMount.call(this)
}
return Component
}如上 HOC 作用仅仅是修改了原来组件原型链上的 componentDidMount 生命周期。但是这样有一个弊端就是如果再用另外一个 HOC 修改原型链上的 componentDidMount ,那么这个HOC的功能即将失效。
类组件中🙅错误写法:
class Index extends React.Component{
render(){
const WrapHome = HOC(Home)
return <WrapHome />
}
}函数组件中🙅错误写法:
function Index(){
const WrapHome = HOC(Home)
return <WrapHome />
}这么写的话每一次类组件触发 render 或者函数组件执行都会产生一个新的WrapHome,react diff 会判定两次不是同一个组件,那么就会卸载老组件,重新挂载新组件,老组件内部的真实 DOM 节点,都不会合理的复用,从而造成了性能的浪费,而且原始组件会被初始化多次。
高阶组件的约定是将所有 props 传递给被包装组件,但这对于 ref 并不适用。那是因为 ref 实际上并不是一个 prop , 就像 key 一样,对于 ref 属性它是由 React 专门处理的。那么如何通过 ref 正常获取到原始组件的实例呢?在 ref 章节已经讲到,可以用 forwardRef做 ref 的转发处理。
多个HOC嵌套,应该留意一下HOC的顺序,还要分析出要各个 HOC 之间是否有依赖关系。
对于 class 声明的类组件,可以用装饰器模式,对类组件进行包装:
@HOC1(styles)
@HOC2
@HOC3
class Index extends React.Component{
/* ... */
}对于函数组件:
function Index(){
/* .... */
}
export default HOC1(styles)(HOC2( HOC3(Index) )) HOC1 -> HOC2 -> HOC3 -> Index
要注意一下包装顺序,越靠近 Index 组件的,就是越内层的 HOC ,离组件 Index 也就越近。
还有一些其他的小细节:
1 如果2个 HOC 相互之间有依赖。比如 HOC1 依赖 HOC2 ,那么 HOC1 应该在 HOC2 内部。
2 如果想通过 HOC 方式给原始组件添加一些额外生命周期,因为涉及到获取原始组件的实例 instance ,那么当前的 HOC 要离原始组件最近。
上述讲到在属性代理 HOC 本质上返回了一个新的 component ,那么如果给原来的 component 绑定一些静态属性方法,如果不处理,新的 component 上就会丢失这些静态属性方法。那么如何解决这个问题呢。
手动继承
当然可以手动将原始组件的静态方法 copy 到 HOC 组件上来,但前提是必须准确知道应该拷贝哪些方法。
function HOC(Component) {
class WrappedComponent extends React.Component {
/*...*/
}
// 必须准确知道应该拷贝哪些方法
WrappedComponent.staticMethod = Component.staticMethod
return WrappedComponent
}引入第三方库
每个静态属性方法都手动绑定会很累,尤其对于开源的 HOC ,对原生组件的静态方法是未知 ,为了解决这个问题可以使用 hoist-non-react-statics 自动拷贝所有的静态方法:
import hoistNonReactStatic from 'hoist-non-react-statics'
function HOC(Component) {
class WrappedComponent extends React.Component {
/*...*/
}
hoistNonReactStatic(WrappedComponent,Component)
return WrappedComponent
}下面用 HOC 实践一个权限拦截场景。具体可以参考如下 demo :
假设期望的效果是:
思路:
第一步,在根部注入权限。
export const Permission = React.createContext([])
export default function Index(){
const [ rootPermission , setRootPermission ] = React.useState([])
React.useEffect(()=>{
/* 获取权限列表 */
getRootPermission().then(res=>{
const { code , data } = res
code === 200 && setRootPermission(data) // [ 'docList' , 'tagList' ]
})
},[])
return <Permission.Provider value={rootPermission} >
<RootRouter/>
</Permission.Provider>
}
[ 'docList' , 'tagList' ])第二步:重点编写HOC
/* 没有权限 */
function NoPermission (){
return <div>您暂时没有权限,请联系管理员开通权限!</div>
}
/* 编写HOC */
export function PermissionHoc(authorization){
return function(Component){
return function Home (props){
const matchPermission =(value,list)=> list.indexOf(value) /* 匹配权限 */
return <Permission.Consumer>
{
(permissionList) => matchPermission(authorization,permissionList) >= 0 ? <Component {...props} /> : <NoPermission />
}
</Permission.Consumer>
}
}
}HOC 编写:
第三步:绑定权限
@PermissionHoc('writeDoc') // 绑定文档录入页面
export default class Index extends React.Component{}export default PermissionHoc('writeTag')(Index) //绑定标签录入页面export default PermissionHoc('tagList')(Index) //绑定标签列表页面export default PermissionHoc('docList')(Index) // 绑定文档列表页面因为上述模拟数据返回的是[ 'docList' , 'tagList' ],所以最终只能看到 标签列表 和 文档列表 页面。
第四步:验证效果
完美实现效果,正常开发可能考虑的因素要比 demo 中的多,demo 实践给大家提供一个思路,具体实现还要看具体的业务需求。
这节主要学习了:
下一节,将一起进入React优化篇。