前言
Ract16后,想去官网看看有啥新的特性,无意间发现官网支持简体中文了,我是有多久没看了(以前学习害得我看繁体~~~),结果发现有些页面醉了,居然没翻译过来,好吧,只能弄自己蹩脚的英文翻译记录一下了。
我们都知道高阶函数, 高阶组件其实是差不多的用法,只不过传入的参数变成了React组件,并返回一个新的组件。
1、含义
官网的解释是:
A higher-order component is a function that takes a component and returns a new component.A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React's compositional nature.
(翻译:高阶组件是一个函数,它接受一个组件并返回一个新组件。高阶组件(HOC)是React中重用组件逻辑的一种高级技术。hoc本身不是React API的一部分,是由 React的组合特性而来的一种设计模式。)
2、实例
先举个🌰~~,一看就懂
class commonCom extends React.Component {
render() {
console.log(this.props, "props");
return <div>commonCom</div>;
}
}
const simpleHoc = (WrappedComponent )=> {
console.log("simpleHoc");
return class extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
};
const NewCom = simpleHoc(commonCom);//调用高阶组件函数
let params = {
id: 123,
name: "kevin"
};
ReactDOM.render(
<NewCom params={params} />,
document.getElementById("root")
);
上例中,组件commonCom 通过simpleHoc的包装,打了一个log。那么simpleHoc就是一个高阶组件了,通过接收一个组件class,并返回一个新的组件class。 在这个函数里,我们可以做很多操作。 而且return的组件同样有自己的生命周期和function。我们也可以把props传给WrappedComponent(被包装的组件),就像父组件给子组件传参一样
注意,HOC不会修改输入组件,也不会使用继承来复制其行为。相反,hoc通过将原始组件包装在容器组件中来组合它。hoc是一个零副作用的纯函数。
3、使用场景
有的时候,我们发现某些组件功能很类似,数据处理也是相同的,只是数据源和显示不同(举个官网的代码实例)
//组件A
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
//组件B
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
上面代码中,CommentList和BlogPost它们的数据源不同并且他们的显示不同。但是我们从代码逻辑上来它们的大部分实现是相同的:
1.在构造器中添加数据的监听;
2.在监听数据方法中当数据改变时调用setState;
3.在组件卸载时移除数据监听。
可以想象,在一个大型应用程序中,上述情况出现的概率很大,订阅数据源和调用setstate的相同模式将反复发生。我们需要一个抽象的、在一个地方定义这个逻辑,并在许多组件之间共享它。这就是高阶组件的优势所在。
这样我们可以编写一个函数withSubscription来订阅数据源的组件,比如commentlist和blogpost。函数将接受作为其参数之一的子组件,该子组件将接收作为属性的订阅数据。
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
这个高阶组件,接受两个参数,第一个是被包装组件;第二个是获取数据的回调方法。改造一下上面的CommentList和BlogPost
//组件A
class CommentList extends React.Component {
render() {
return (
<div>
{this.props.data.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
//组件B
class BlogPost extends React.Component {
render() {
return <TextBlock text={this.props.data} />;
}
}
上面实例中,被包装的组件CommentList和BlogPost接收容器的所有属性,以及一个新的属性数据,用于呈现其输出。HOC与数据的使用方式和使用原因无关,而封装组件与数据的来源无关。就这样,组件的功能更加的简洁和专注。
因为withSubscription是一个普通函数,所以您可以添加任意多个或任意少的参数。例如,您可能希望使dataprop的名称可配置,以进一步将hoc与包装的组件隔离开来。或者您可以接受配置shouldComponentUpdate的参数,或者接受配置数据源的参数。这些都是可能的,因为hoc可以完全控制如何定义组件。
另外,与组件一样,withsubscription和wrapped组件(上例中的CommentList和BlogPost)之间的契约完全基于props。这使得将一个hoc替换为另一个hoc变得容易,只要它们为被包装的组件提供相同的属性。如果更改数据提取库,不改变数据字段,只需修改withsubscription即可,作为数据展示的wrapped组件(上例中的CommentList和BlogPost)无需修改。
4、HOC的实现方式
4.1 属性代理(Props Proxy)的形式
通过HOc包装wrappedComponent,本来传给wrappedComponent的props,都在HOC中接受到了,也就是props proxy。 由此我们可以做一些操作:
一、操作props
最直观的就是接受到props,我们可以做任何读取,编辑,删除的很多自定义操作。包括hoc中定义的自定义事件,都可以通过props再传下去。
const simpleHoc = WrappedComponent => {
return class extends React.Component {
render() {
let mParams={"title":'new params'};
return <WrappedComponent { ...this.props} mNewParams={mParams}/>;
}
};
};
二、refs获取组件实例
当我们包装wrappedComponent的时候,想获取到它的实例怎么办,可以通过引用(ref),在wrappedComponent组件挂载的时候,会执行ref的回调函数,在hoc中取到组件的实例。这样它的props, state,方法都是可以取到的。可以参考我的另一篇关于《Refs和Refs 转发》的文章。这里就不举例子了,文章估计有点长~~~
三、抽离state
通过props传递回调函数,操作wrappedComponent组件的state.用的比较多的就是react处理表单的时候。通常react在处理表单的时候,一般使用的是受控组件,即把input都做成受控的,改变value的时候,用onChange事件同步到state中。
4.2 反向继承
反向继承(Inheritance Inversion),简称II。跟属性代理的方式不同的是,II采用通过 去继承WrappedComponent,本来是一种嵌套的关系,结果II返回的组件却继承了WrappedComponent,这看起来是一种反转的关系。
通过继承WrappedComponent,除了一些静态方法,包括生命周期,state,各种function,我们都可以得到。
class commonCom extends React.Component {
constructor() {
super();
this.state = {
comName: "commonCom"
};
}
componentDidMount() {
console.log("didMount");
}
render() {
// console.log("props:" + JSON.stringify(this.props));
return <div>commonCom</div>;
}
}
const simpleHoc = WrappedComponent =>
class extends WrappedComponent {
render() {
console.log("state:",this.state);//state: {comName: "commonCom"}
return super.render();
}
};
simpleHoc return的组件通过继承,拥有了commonCom 的生命周期及属性,所以didMount会打印,state也通过constructor执行,得到state.comName。
5、注意点(Convention:惯例)
5.1 不要在HOCs中修改原组件原型(或对其进行其他修改)
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
};
// The fact that we're returning the original input is a hint that it has
// been mutated.
return InputComponent;
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
上例中,在高阶组件logProps中为修改了原组件InputComponent的原型方法componentWillReceiveProps,有的时候,根据不同的需求,一个组件在不同场景下需要不同的HOC进行包装,要是再有一个HOC组件也修改了原组件InputComponent的原型的属性或者方法,那么logProps修改的功能将会被覆盖。
所以,记住
HOCs won't work with function components, which do not have lifecycle methods.:
高阶组件不能用于没有生命周期方法的功能组件
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...this.props} />;
}
}
}
改成上例的形式,既有上面的功能,同时避免了潜在的冲突。它是一个纯函数,所以它可以与其他hoc组合,甚至与自身组合。
有没有发现,HOCs和container components(容器组件)的模式相似。容器组件是分离高级别和低级别关注点之间责任的策略的一部分。容器管理订阅和状态等内容,并将属性传递给处理呈现UI等内容的组件。HOCs使用容器作为其实现的一部分。您可以将hocs视为参数化容器组件定义。
5.2 传递参数
HOCs向组件添加参数时,不要把和自己相关的参数传递过去,另外自己注入的参数要和外部传递给包装组件的分开。
render() {
// Filter out extra props that are specific to this HOC and shouldn't be
// passed through
const { extraProp, ...passThroughProps } = this.props;
// Inject props into the wrapped component. These are usually state values or
// instance methods.
const injectedProp = someStateOrInstanceMethod;
// Pass props to wrapped component
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
这个惯例有助于确保HOCs尽可能灵活和可重用。
5.3 不要再render方法中使用HOCs
react的diffing算法(称为和解)使用组件标识来确定它是应该更新现有的子树,还是丢弃它并装载一个新的子树。如果从render返回的组件与上一个render返回的组件相同(===),那么react会通过将其与新的组件进行比较来递归更新子树。如果它们不相等,则会完全卸载前一个子树。
render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}
这里的问题不仅仅是性能问题——重新安装组件会导致该组件及其所有子组件的状态丢失。相反,在组件定义之外应用hocs,以便生成的组件只创建一次。然后,它的身份将在渲染中保持一致。在那些需要动态应用hoc的罕见情况下,您也可以在组件的生命周期方法或其构造函数内进行。
5.4 复制被包装组件的静态方法
将hoc应用于组件时,原始组件将被容器组件包装。这意味着新组件没有原始组件的任何静态方法。
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);
// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
要解决此问题,可以在返回容器之前将方法复制到容器上。(不推荐)
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// Must know exactly which method(s) to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
那么问题来了,我们就需要确切地知道需要复制哪些方法。这时,我们可以使用hoist-non-react-statics,自动复制所有non-React 的静态方法。(react-router 里withRouter就使用了这个包)
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
另一种可能的解决方案是将静态方法与组件本身分开导出
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...export the method separately...
export { someFunction };
// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
5.5 最大化可组合性
并非所有的HOCs都是一样的。有时,它们只接受一个参数,即封装的组件。
const NavbarWithRouter = withRouter(Navbar);
通常,hocs接受额外的参数。
const CommentWithRelay = Relay.createContainer(Comment, config);
上例中,Relay中的高阶组件依赖于一个配置对象config。
最常见的形式类似于Redux中的connect方法
// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
感觉晕,看不懂?那我们就拆开看看
// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is a HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);
这样就很明显,connect方法返回的是个函数。换句话说,connect函数是一个返回高阶组件的高阶函数!
这种形式可能看起来很混乱,但它有一个有用的特性,像connect函数返回的单参数hoc具有 Component => Component的特征。输出类型与输入类型相同的函数很容易组合在一起。
// Instead of doing this...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
// These are both single-argument HOCs
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
(同样的属性也允许将connect和其他增强型hoc用作装饰器,这是一个实验性的javascript提议)
compose实用程序功能由许多第三方库提供,包括lodash(作为lodash.flowright)、redux和ramda。
5.6 Refs 穿透不到被包装的组件
虽然高阶组件的约定是将所有属性传递给封装的组件,但这对refs不起作用。这是因为refis不是真正的props,它类似于key,由react专门处理的。如果向其组件是HOC组件的元素添加引用,则引用的是最外层容器组件的实例,而不是包装组件。可以使用react.forwardRef API。可以参考我的另一篇关于《Refs和Refs 转发》的文章。
6、总结
高阶组件最大的好处就是解耦和灵活性,在react的开发中还是很有用的。这里只是按照官网以及自己的理解记录。掌握它的技巧,了解它的限制,避开它的缺点,结合自己的应用场景,灵活运用,会有意想不到的效果。
(多多包涵,尽量用通俗的话写了,各种翻译软件,总觉得有些名词翻译的有问题~~~o(∩_∩)o 哈哈~~~)