具体而言,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
我们需要一个抽象,允许我们在一个地方定义逻辑,并在许多组件之间共享它。这正是高阶组件擅长的地方。
优点:HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
属性代理:高阶组件通过被包裹的React组件来操作props
开发过程中,我们都见过这样的写法,一个Provider组件,传递一个store,在它的子组件内部都可以通过this.props访问到store里面的内容,下面就来分析一下是怎么实现的。
首先介绍一种传递属性的方式,上下文:
平时我们会看到这样的代码
定义一个store
const store = {
name: 'ryan',
age:10
}
定一个Provider组件,定义上下文。我门先通过上下文传递数据。
import React from "react";
import PropTypes from 'prop-types'
class Provider extends React.Component{
getChildContext () {
return this.props.store
}
/** static childContextTypes = {
name: PropTypes.string,
age: PropTypes.number
} **/ // 目前class还不支持这样的方式写静态属性 ts环境可以这么写
constructor(props){
super(props)
this.state = {
name: 'provider-user'
}
}
render() {
return this.props.children
}
}
// 这样添加静态属性
Provider.childContextTypes = {
name: PropTypes.string,
age: PropTypes.number
}
定一个BaseUser组件
import React from "react";
import PropTypes from 'prop-types'
class BaseUser extends React.Component{
// static contextTypes = {
// age: PropTypes.number
// }
render() {
return (
<div>
{this.context.age}
</div>
);
}
}
BaseUser.contextTypes = {
age: PropTypes.number
}
export default BaseUser
定一个BasePost组件
import React from "react";
import PropTypes from 'prop-types'
class BasePost extends React.Component{
// static contextTypes = {
// name: PropTypes.string
// }
render() {
return (
<div>
{this.context.name}
</div>
);
}
}
BasePost.contextTypes = {
name: PropTypes.string
}
export default BasePost
import React from "react";
import BaseUser from './components/base'
import BasePost from './components/post'
import Provider from './components/provider'
class App extends React.Component {
render() {
return (
<Provider store={store}>
<div>
<BaseUser/>
<BasePost/>
</div>
</Provider>
);
}
}
这样可以将store里面的数据传递到Provider组件的子组件当中。但是每一子组件都要加一段相似的代码,就是上下文,这样就会很繁琐,我们可以试着把这段代码抽离出来,来定义一个connect组件。一个高阶组件,传递一个组件并返回一个新的组件。
import React from "react";
import PropTypes from 'prop-types'
import Provider from './Provider/index'
const connect = (Com) => {
class ConnectComponent extends React.Component {
render() {
return (
<Com {...this.context}/>
);
}
}
ConnectComponent.contextTypes = Provider.childContextTypes;
return ConnectComponent
}
这样,就可以去掉子组件中很类似的部分。提取公共属性,传递给props。
子组件使用一下这个高阶组件,就可以直接props访问状态。
const User = connect(BaseUser)
const Post = connect(BasePost)
import React from "react";
class BasePost extends React.Component{
componentDidMount() {
console.log(this.props) //{name: "ryan", age: 10}
}
render() {
return (
<div>
{this.props.name}
</div>
);
}
}
export default BasePost
import React from "react";
class BaseUser extends React.Component{
componentDidMount() {
console.log(this.props) //{name: "ryan", age: 10}
}
render() {
return (
<div>
{this.props.age}
</div>
);
}
}
export default BaseUser
可以看到,store中的属性都注入到每一个子组件中
app组件直接渲染处理后的组件。
class App extends React.Component {
render() {
return (
<Provider store={store}>
<div>
<User/>
<Post/>
</div>
</Provider>
);
}
}
但是又有一个问题,这样操作会把store内部所有的状态都注入到了所有的子组件中,而有些组件并不会用到这些属性,会增传输成本, 下面就来实现一个函数,让状态的可以有选择的注入到组件中。
实现一个inject组件,这个组件接收一个key,返回一个高阶组件,接受一个组件。这里不能再用上下文,这个函数内部拿不到上下文的值,需要换一种写法灵活应用。我们需要的不只是把一个参数注入到组件,还需要在属性变化的时候更新组件:
import React,{Component} from "react";
// 用于预先 将业务组件,进行数据的封装,便于我们 方便获取数据
let store = {
name: 'ryan',
age: 10
}
const inject = key => Com => {
class connectComponent extends Component {
constructor(props){
super(props)
this.state = {
[key]: store[key]
}
}
componentDidMount() {
let that = this
window.store = new Proxy(store, {
get: function (target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
that.setState({
[key]:value
})
return Reflect.set(target, key, value, receiver);
}
})
}
render() {
return <Com {...this.state}/>
}
}
return connectComponent
}
export default inject
返回一个新的组件。inject组件内部用proxy监听store的变化。如果改变就更新组件的state,是组件重新渲染,听起来是不是有点像mobx,redux一类的状态管理器。
我们来实现一下app组件
@inject('age')
class User extends Component{
render() {
return <div>{this.props.age}</div>
}
}
// 相当于 User = inject('age)(User)
class App extends Component{
render() {
return <User/>
}
}
export default App
反向继承: 高阶组件继承于被包裹的React组件
一个高阶组件,继承传入的组件,并返回一个新的组件。
import React,{Component} from "react";
// 用于预先 将业务组件,进行数据的封装,便于我们 方便获取数据
// 反向继承 交互的封装,
const loading = Com => {
class LoadingComponent extends Com {
constructor (props) {
super(props)
this.state = {
name: '麦乐'
}
}
showLoading(){
console.log('loading')
}
hideLoading(){
console.log('hide')
}
}
return LoadingComponent
}
@loading
class User extends Component{
constructor (props) {
super(props)
this.state = {
name: 'maile'
age: 18
}
}
showLoading() {
console.log('_loading')
}
render() {
return <div>user</div>
}
componentDidMount() {
console.log(this.state.name) // 麦乐 本组件内状态失效
console.log(this.state.age) // undefined
this.showLoading() // loading 本组件内部的同名函数不再生效
this.hideLoading() // hide
}
}
class App extends Component{
render() {
return <User/>
}
}
export default App
这是因为loading返回的组件继承了user组件,user组件内部同名属性和方法将会被覆盖。如果想要user组件内部的状态生效,loding高阶组件内部就不能设置状态,使用的时候灵活应用。