HOC真的就那么高级吗|博客系列
# 1. HOC的概念
其实如果有过React开发经验的小伙伴对HOC的概念应该就不陌生了,不过既然是介绍它的话,那也稍微正式一点:
HOC,全称Higher-Order Components,即高阶组件。
它的概念应该是来源于JavaScript的高阶函数,我们知道高阶函数就是接受函数作为输入或者输出的函数。
通俗来说就是一个函数,它的参数可以是一个函数,它的返回值也可以是一个函数😄,这样的函数就被称为高阶函数。
例如🌰下面的这两个函数:
// 1. 参数为函数
const test1 = fn => {
setTimeout(() => fn(), 1000)
};
const log1 = () => console.log('我爱学习');
test1(log1); // 1s后打印
// 2. 返回值为函数
const test2 = () => {
const log2 = name => console.log(name);
return log2;
}
test2()('学习不爱我'); // 理解打印
那么其实,高阶组件它也仅仅只是一个接受组件作为输入并返回组件的函数。认为它并不是一个新的API或者一个新的什么玩意,仅仅是一种模式吧,或者说是一种技巧,这种技巧能够帮助我们复用组件逻辑。
就像下面👇这样的用法:
让我们来创建一个FinalComponent.js
import React from 'react';
function MyHOC (WrappedComponent) {
return class extends React.Component {
render () {
return <WrappedComponent />;
}
}
}
class TestComponent extends React.Component {
render () {
return (
<div>我就是个普通的组件</div>
)
}
}
const FinalComponent = MyHOC(TestComponent);
export default FinalComponent;
在这个案例中,我做了这么几件事:
- 创建了一个名为
MyHOC的函数,它接收一个名为WrappedComponent的参数,并返回一个新的匿名组件 - 这个匿名组件的
render返回的是传递进来的WrappedComponent组件 - 之后创建了一个名为
TestComponent的组件 - 再调用
MyHOC函数并把TestComponent传递进去赋值给FinalComponent变量 - 此时的
FinalComponent其实就是那个匿名组件,我们将它导出。
在其它地方使用FinalComponent这个组件的话,就能正常渲染出"我就是个普通的组件"了。
可以看到,上面👆的这种用法其实就叫高阶组件,它首先需要定义一个函数,然后这个函数接收一个组件并返回一个新的组件。
大家要注意这里的命名哟,MyHOC函数的参数必须大写开头的,因为后面需要把它当成组件来返回,而我们知道,在React中如果是组件的话,它的命名开头必须是要大写,React会将小写开头的组件当成普通的HTML标签处理,这样就会报错。
(另外还有一点,HOC并不是React里独有的,其它框架也可以使用,比如晨曦老哥的这篇文章就介绍了它在Vue中的用法:Vue 进阶必学之高阶组件 HOC)
好的,既然在上面谈到了高阶组件主要是可以帮助我们复用组件逻辑,那大家会不会想到另一个叫Mixin的东西呢?但是因为ES6本身是不包含任何Mixin支持的,所以当你在React中使用ES6 class时,将不支持Mixin ,而且使用它本身会有很多问题,现在也是不推荐使用了。
既然你把高阶组件吹的这么牛,那它具体怎么用呢?
咦~这么着急干嘛?哈哈哈哈,咱接着往下看。
表情包不要着急
# 2. 如何实现高阶组件
# 2.1 属性代理
其实在上面👆已经向大家展示了高阶组件的基本用法,让我们来简单回顾一下前面是怎么做的:
import React from 'react';
function MyHOC (WrappedComponent) {
return class extends React.Component {
render () {
return <WrappedComponent />;
}
}
}
class TestComponent extends React.Component {
render () {
return (
<div>我就是个普通的组件</div>
)
}
}
const FinalComponent = MyHOC(TestComponent);
export default FinalComponent;
定义一个函数(MyHOC)且接收一个组件,之后返回一个新的组件。
那大家试想一下,如果此时我在页面中引用了FinalComponent组件,并且需要向TestComponent传递一些属性,也就是props,该怎么做呢?
<FinalComponent id={1} />
通过这样传递的id虽然不能直接被TestComponent组件给拿到,但是却可以在MyHOC中拿到,因为此时FinalComponent确实就是MyHOC函数中导出的那个匿名组件,这样的话,我们就可以通过this来访问到这个匿名组件的一些属性,包括使用这个组件时传递的一些props:
function MyHOC (WrappedComponent) {
return class extends React.Component {
render () {
console.log(this.props); // { id: 1 }
return <WrappedComponent />;
}
}
}
好的👌!我们已经成功拿到调用FinalComponent时传递的props,接下里需要把它传递给WrappedComponent,这就很简单了,只需要使用ES6的对象展开操作符即可实现,也就是这样:
function MyHOC (WrappedComponent) {
return class extends React.Component {
render () {
console.log(this.props); // 一、{ id: 1 }
return <WrappedComponent {...this.props} />;
}
}
}
class TestComponent extends React.Component {
render () {
return (
console.log(this.props); // 二、在这里能拿到
<div>我就是个普通的组件</div>
)
}
}
这个过程其实就是一个浅拷贝的过程,如果this.props有多个属性的话,都会将其展开传递给WrappedComponent:
console.log(this.props); // { id: 1, uid: 123 }
<WrappedComponent {...this.props} />
// 等价于 =>
<WrappedComponent id={1} uid={123} />
可以看到,在每次调用WrappedComponent组件的时候,都必然要经过MyHOC函数,也就是说MyHOC成了WrappedComponent的"代理",那么我们是不是就可以对在这一层做一些额外的操作,例如操作前面提到的this.props,或者是WrappedComponent的静态属性方法。
像这种函数返回一个我们自己定义的组件,然后在render中返回要包裹的组件,同时在函数中做一些额外处理的方式,我们就称之为属性代理,根据它的功能来看这个名字是不是很好理解呢?😊。
一起来看个简写:
function proxyHOC(WrappedComponent) {
return class extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
表情包开心
好的👌,既然已经告诉了你们这种全新的使用方式了,那你能想到通过这种名为"属性代理"的东西能让我们做哪些好玩有趣的事吗?
首先第一点来给你起个头:在我们定义的高级组件的那个函数中,是可以对要返回的组件的属性进行二次加工的,那我们是不是就可以给this.props添加上一些新的属性,并传递给WrappedComponent?OK👌,让我们来看看"属性代理"它的第一种用法。
# 2.1.1 操作props
就像上面👆说的,我们可以给this.props添加上一些新的属性并向下传递,但是这个添加不是让你去直接修改this.props哈,比如下面👇这种用法肯定就是不行的了:
render () {
this.props.remark = '别自闭'
return <WrappedComponent {...this.props} />
}
因为React是单向数据流,它不允许你去修改props,因此你会发现控制台直接就报错了:
HOC21.jsx:10 Uncaught TypeError: Cannot add property remark, object is not extensible
那好的,我新建一个对象再添加额外属性就不行了😄?比如我们可以把前面的案例改造一下,这样做:
import React from 'react';
function MyHOC (WrappedComponent) {
return class extends React.Component {
render () {
const newProps = { // 重点看这里
...this.props,
remark: '别自闭'
}
return <WrappedComponent {...newProps} />
}
}
}
class TestComponent extends React.Component {
render () {
console.log(this.props) // { id: 1, remark: '别自闭' }
return <div>我就是个普通div</div>
}
}
const FinalComponent = MyHOC(TestComponent);
export default FinalComponent;
使用:
<FinalComponent id={1} />
大家可以看到,虽然FinalComponent我们在调用的时候只传递了一个属性,也就是id,但是最终TestComponent组件接收到的props却经过MyHOC添加上了额外的属性"自闭",呸,是"别自闭"。
这样就达到了在调用TestComponent的时候,可以额外新加一些属性的功能。例如现在如果我们有好几个组件,都需要添加一些相同的属性,那么我们是不是只需要定义好一个MyHOC,然后让这些组件都经过MyHOC过一遍就可以了。
Good boy!现在我们已经会"属性代理"的其中一种用法了,不说了,学累了,喝口水去,顺便想想还可以怎样用。
表情包休息时间
# 2.1.2 组合渲染
唔,欢迎回来呀。就在刚刚看表情包的时间,大家有想到什么其它的用法不?
