React 高阶组件

React 高阶组件

  • 参考:王红元老师的React

高阶函数

  • 高阶函数的维基百科定义:至少满足以下条件之一:
    • 接受一个或多个函数作为输入;
    • 输出一个函数;
  • JavaScript中比较常见的filter、map、reduce都是高阶函数。
  • 那么说明是高阶组件呢?
  • 高阶组件的英文是 Higher-Order Components,简称为 HOC
  • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数
  • 我们可以进行如下的解析:
    • 首先, 高阶组件 本身不是一个组件,而是一个函数;
    • 其次,这个函数的参数是一个组件,返回值也是一个组件;

高阶组件的定义

import React, { PureComponent } from 'react'

class App extends PureComponent {
  render() {
    return (
      <div>
        App: {this.props.name}
      </div>
    )
  }
}

function enhanceComponent(WrappedComponent) {
  class NewComponent extends PureComponent {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }

  NewComponent.displayName = "Kobe";
  return NewComponent;
}

function enhanceComponent2(WrappedComponent) {
  function NewComponent(props) {
    return <WrappedComponent {...props}/>
  }

  NewComponent.displayName = "Kobe";
  return NewComponent;
}

const EnhanceComponent = enhanceComponent2(App);
export default EnhanceComponent;
  • 组件的名称问题:
    • 在ES6中,类表达式中类名是可以省略的;
    • 组件的名称都可以通过displayName来修改;
  • 高阶组件并不是React API的一部分,它是基于React的 组合特性而形成的设计模式;
  • 高阶组件在一些React第三方库中非常常见:
    • 比如redux中的connect;(后续会讲到)
    • 比如react-router中的withRouter;(后续会讲到)

应用一-增强props

  • 不修改原有代码的情况下,添加新的prop
import React, { PureComponent } from 'react';

// 定义一个高阶组件
function enhanceRegionProps(WrappedComponent) {
  return props => {
    return <WrappedComponent {...props} region="China" />
  }
}

class Home extends PureComponent {
  render() {
    return <h2>Home: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}


class About extends PureComponent {
  render() {
    return <h2>About: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}


const EnhanceHome = enhanceRegionProps(Home);
const EnhanceAbout = enhanceRegionProps(About);

class App extends PureComponent {
  render() {
    return (
      <div>
        App
        <EnhanceHome nickname="coderwhy" level={90} />
        <EnhanceAbout nickname="kobe" level={99} />
      </div>
    )
  }
}

export default App;
  • 利用高阶组件来共享Context
import React, { PureComponent, createContext } from 'react';

// 定义一个高阶组件
function withUser(WrappedComponent) {
  return props => {
    return (
      <UserContext.Consumer>
        {
          user => {
            return <WrappedComponent {...props} {...user}/>
          } 
        }
      </UserContext.Consumer>
    )
  }
}

// 创建Context
const UserContext = createContext({
  nickname: "默认",
  level: -1,
  区域: "中国"
});

class Home extends PureComponent {
  render() {
    return <h2>Home: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}


class About extends PureComponent {
  render() {
    return <h2>About: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
  }
}

class Detail extends PureComponent {
  render() {
    return (
      <ul>
        <li>{this.props.nickname}</li>
        <li>{this.props.level}</li>
        <li>{this.props.region}</li>
      </ul>
    )
  }
}


const UserHome = withUser(Home);
const UserAbout = withUser(About);
const UserDetail = withUser(Detail);

class App extends PureComponent {
  render() {
    return (
      <div>
        App
        <UserContext.Provider value={{nickname: "why", level: 90, region: "中国"}}>
          <UserHome/>
          <UserAbout/>
          <UserDetail/>
        </UserContext.Provider>
      </div>
    )
  }
}

export default App;
  • 补充语法(类表达式):

 应用二 – 渲染判断鉴权

  • 在开发中,我们可能遇到这样的场景:
  • 某些页面是必须用户登录成功才能进行进入;
    • 如果用户没有登录成功,那么直接跳转到登录页面;
    • 这个时候,我们就可以使用高阶组件来完成鉴权操作:
import React, { PureComponent } from 'react';

class LoginPage extends PureComponent {
  render() {
    return <h2>LoginPage</h2>
  }
}

function withAuth(WrappedComponent) {
  const NewCpn = props => {
    const {isLogin} = props;
    if (isLogin) {
      return <WrappedComponent {...props}/>
    } else {
      return <LoginPage/>
    }
  }

  NewCpn.displayName = "AuthCpn"

  return NewCpn;
}

// 购物车组件
class CartPage extends PureComponent {
  render() {
    return <h2>CartPage</h2>
  }
}

const AuthCartPage = withAuth(CartPage);

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <AuthCartPage isLogin={true}/>
      </div>
    )
  }
}

应用三 – 生命周期劫持

import React, { PureComponent } from 'react';

function withRenderTime(WrappedComponent) {
  return class extends PureComponent {
    // 即将渲染获取一个时间 beginTime
    UNSAFE_componentWillMount() {
      this.beginTime = Date.now();
    }

    // 渲染完成再获取一个时间 endTime
    componentDidMount() {
      this.endTime = Date.now();
      const interval = this.endTime - this.beginTime;
      console.log(`${WrappedComponent.name}渲染时间: ${interval}`)
    }

    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

class Home extends PureComponent {
  render() {
    return <h2>Home</h2>
  }
}


class About extends PureComponent {
  render() {
    return <h2>About</h2>
  }
}

const TimeHome = withRenderTime(Home);
const TimeAbout = withRenderTime(About);

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <TimeHome />
        <TimeAbout />
      </div>
    )
  }
}

class Person {

}

console.log(Person.name);  //类默认属性 name

高阶函数的意义

  • HOC也有自己的一些缺陷:
    • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
    • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;
  • Hooks的出现,是开创性的,它解决了很多React之前的存在的问题
    • 比如this指向问题、比如hoc的嵌套复杂度问题等等;

ref的转发

  • 在前面我们学习ref时讲过,ref不能应用于函数式组件:
    • 因为函数式组件没有实例,所以不能获取到对应的组件对象
  • 但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?
    • 方式一:直接传入ref属性(错误的做法)
    • 方式二:通过forwardRef高阶函数;
import React, { PureComponent, createRef, forwardRef } from 'react';

class Home extends PureComponent {
  render() {
    return <h2>Home</h2>
  }
}

// 高阶组件forwardRef
const Profile = forwardRef(function(props, ref) {
  return <p ref={ref}>Profile</p>
})

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.titleRef = createRef();
    this.homeRef = createRef();
    this.profileRef = createRef();
  }

  render() {
    return (
      <div>
        <h2 ref={this.titleRef}>Hello World</h2>
        <Home ref={this.homeRef}/>

        <Profile ref={this.profileRef} name={"why"}/>

        <button onClick={e => this.printRef()}>打印ref</button>
      </div>
    )
  }

  printRef() {
    console.log(this.titleRef.current);
    console.log(this.homeRef.current);
    console.log(this.profileRef.current);
  }
}

Portals的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。
  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
    • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
    • 第二个参数(container)是一个 DOM 元素;

  • 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
  • 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

Modal组件案例

  • 比如说,我们准备开发一个Modal组件,它可以将它的子组件渲染到屏幕的中间位置:
    • 步骤一:修改index.html添加新的节点
    • 步骤二:编写这个节点的样式
    • 步骤三:编写组件代码
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';

class Modal extends PureComponent {
  render() {
    return ReactDOM.createPortal(
      this.props.children,
      document.getElementById("modal")
    )
  }
}

class Home extends PureComponent {
  render() {
    return (
      <div>
        <h2>Home</h2>
        <Modal>
          <h2>Title</h2>
        </Modal>
      </div>
    )
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home/>
      </div>
    )
  }
}
  • 样式:
#modal {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

fragment

  • 在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素:

  • 我们又希望可以不渲染这样一个div应该如何操作呢?
    • 使用Fragment
    • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
  • React还提供了Fragment的短语法:
    • 它看起来像空标签 <> </>
    • 但是,如果我们需要在Fragment中添加key,那么就不能使用短语法
import React, { PureComponent, Fragment } from 'react';

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0,
      friends: [
        {name: "why", age: 18},
        {name: "lilei", age: 20},
        {name: "kobe", age: 25},
      ]
    }
  }

  render() {
    return (
      <>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <div>
          {
            this.state.friends.map((item, index) => {
              return (
                <Fragment key={item.name}>
                  <div>{item.name}</div>
                  <p>{item.age}</p>
                  <hr/>
                </Fragment>
              )
            })
          }
        </div>
      </>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

StrictMode

  • trictMode 是一个用来突出显示应用程序中潜在问题的工具。
    • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
    • 它为其后代元素触发额外的检查和警告;
    • 严格模式检查仅在开发模式下运行;它们不会影响生产构建;
  • 可以为应用程序的任何部分启用严格模式:
    • 不会对 Header 和 Footer 组件运行严格模式检查;
    • 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查

  •  严格模式检查的是什么?
    • 1.识别不安全的生命周期:
    • 2.使用过时的ref API
    • 3.使用废弃的findDOMNode方法
      • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了。
    • 4.检查意外的副作用
      • 这个组件的constructor会被调用两次;
      • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
      • 在生产环境中,是不会被调用两次的;
    • 5.检测过时的context API
      • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值