参数传递
高阶组件的参数并非只是一个组件,它还可以接受其他参数。
function withPersistentData(WrappedComponent, key) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem(key);
this.setState({
[`local/${key}`]: data,
});
}
render() {
// 通过{...this.props}把传递给当前组件的属性继续传递给被包装的组件
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}HOC(...params)返回值是一个高阶组件,高阶组件需要的参数是先传递给 HOC 函数的。用这种形式改写 withPersistentData 如下
const withPersistentData = key => WrappedCompnent => {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem(key);
this.setState({ data });
}
render() {
// 通过 {...this.props} 把传递给当前组件的属性继续传递给被包装的组件
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
};
class MyComponent extends Component {
render() {
return <div>{this.props.data}</div>;
}
}
// 获取key='data'的数据
const MyComponentPersistentData = withPersistentData('data')(MyCompoent);实际上,这种形式的高阶组件大量出现在第三方库中,如 react-redux 的 connect 函数就是一个典型的例子。connect 的简化定义如下:
connect(mapStateToProps, mapDispatchToProps)(WrappedComponent)
这个函数会将一个 React 组件连接到 Redux 的 store 上,在连接的过程中,connect 通过函数参数 mapStateToProps 从全局 store 中取出当前组件需要的 state,并把 state 转化成当前组件的 props,传递给当前组件。 connect 并不会修改传递进去的组件的定义,而是会返回一个新的组件。
connect 的参数 mapStateToProps、mapDispatchToProps 是函数类型,说明高阶组件的参数也可以是函数类型。
const connectedComponentA = connect(mapStateToProps, mapDispatchToProps)(ComponentA)
我们可以把它拆分来看
// connect是一个函数,返回值 enhance也是一个函数
const enhance = connect(mapState, mapDispatch)
// enhance是一个高阶组件
const ConnectedComponentA = enhance(ComponentA)
这种形式的高阶组件易组合使用。因为当多个函数的输出和它的输入类型相同时,这些函数易组合到一起。
// connect的参数是可选参数,这里省略了mapDispatch参数
const ConnectedComponentA = connect(mapStateToProps)(withLog(ComponentA))
我们还可以定义一个工具函数 compose(...funcs)
function compose(...fns) {
if (fns.length === 0) return arg => arg
if (fns.length === 1) return fns[0]
return fns.reduce((acc, cur) => (...args) => acc(cur(args)))调用compose(f, g, h) 等价于 (...args) => f(g(h(...args)))
用 compose 函数可以把高阶组件嵌套的写法打平:
const enhance = compose(
connect(mapState),
withLog(),
);
const ConnectedComponentA = enhance(ComponentA);像 Redux 等第三方库都提供了 compose 的实现,compose 结合高阶组件使用可以显著提高代码的可读性和逻辑的清晰度
继承方式实现高阶组件
上面这类由高阶组件处理通用逻辑,然后再将相关属性传递给被包装组件,我们称这种实现方式为属性代理。
除此外,还可以通过继承方式实现高阶组件,通过继承被包装组件实现逻辑的复用。继承方式实现的高阶组件常用于渲染劫持。例如:当用户处于登录状态,允许组件渲染,否则渲染一个空组件:
function withAuth(WrappedComponent) {
return class extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render();
} else {
return null;
}
}
};
}根据 WrappedComponent 的this.props.loggedIn判断用户是否已经登录,若登录就通过 super.render()调用 WrappedComponent 的 render 方法正常渲染,否则返回 null。
继承方式实现的高阶组件对被包装组件具有侵入性,当组合多个高阶组件使用时,很容易因为子类组件忘记通过 super 调用父类组件方法而导致逻辑丢失。因此,在使用高阶组件时,应尽量通过代理方式实现高阶组件。
注意事项
1. 为了在开发和调试阶段更好地区别包装了不同组件的高阶组件,需要对高阶组件的显示名称做自定义处理。
常用的处理方式是,把被包装组件的显示名称也包到高阶组件的显示名称中:
function withPersistentData(WrappedComponent) {
return class extends Component {
// 结合被包装组件的名称,自定义高阶组件的名称
static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
render() {
return; // 省略
}
};
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}2. 不要在组件的 render 方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。
因为调用高阶组件,每次都会返回一个新的组件,于是每次 render 前一次高阶组件创建的组件都会被卸载 unmount,然后重新挂载(mount)本次创建的新组件,影响效率又丢失子组件状态,如下:
render() {
// 每次render, enhance都会创建一个新组件,尽管被包装的组件没有变
const EnhancedComponent = enhance(MyComponent)
// 因为是新的组件,所以会经历旧组件的卸载和新组件的重新挂载
return <EnhancedComopnent />
}所以高阶组件最适合的地方是在组件定义的外部,这样就不会收到组件生命周期的影响。
3. 如果需要使用被包装组件的静态方法,那么必须手动复制这些静态方法。
因为高阶组件返回的新组件不包含被包装组件的静态方法,如:
// WrappedComponent组件定义了一个静态方法staticMethod
WrappedComponent.staticMethod = function() {
// ...
};
function withHOC(WrappedComponent) {
class Enhance extends Component {
// ...
}
// 手动复制静态方法到Enhance上
Enhance.staticMethod = WrappedComponent.staticMethod;
}4. refs 不会被传递给被包装组件
尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是 ref 并不会传递给被包装组件。如果在高阶组件的返回数组中定义了 ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果希望获取被包装组件的引用,那么可以自定义一个属性,属性的值是一个函数,传递给被包装的 ref。下面的例子就是用 inputRef 这个属性名替代常规的 ref 命名:
function FocusInput({ inputRef, ...rest }) {
// 使用高阶组件传递的inputRef作为ref的值
return <input ref={inputRef} {...rest} />;
}
// enhance是一个高阶组件
const EnhanceInput = enhance(FocusInput);
// 在一个组件的render方法中,自定义属性inputRef代替ref
// 保证inputRef可以传递给被包装组件
return <EnhanceInput inputRef={input => (this.input = input)} />;
// 组件内 让FocusInput自动获取焦点
this.input.focus();5. 与父组件的区别
高阶组件在一些方面与父组件类似。我们可以把高阶组件中的逻辑放到一个父组件中执行,执行完成的结果再传递给子组件,但是高阶组件强调的是逻辑的抽象。
高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是 UI/DOM。如果逻辑是与 DOM 直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与 DOM 不直接相关的,那么这部分逻辑使用用高阶组件抽象,如数据校验、请求发送等。
总结
高阶组件用于封装组件的通用逻辑,常用在操作组件 props、通过 ref 访问组件实例、组件状态提升和用其他元素包装组件等场景中。
高阶组件可以接受被包装组件以外的其他参数,多个高阶组件还可以组合使用。高阶组件一般通过代理实现,少量场景中也会使用继承等方式实现。灵活地使用高阶组件可以显著提高代码质量和效率。
