「React」之组件逻辑复用小技巧

原创
09/12 17:54
阅读数 2.3K

编者荐语:

本文将介绍React组件逻辑复用的一些常用模式和技巧。包括一下几个方面:

  • 什么是高阶组件 HOC
  • HOC解决了哪些问题
  • 如何封装一个简单的高阶组件
  • HOC在项目中常用的一些技巧和方法
  • 什么是 Render Props
  • Render Props的特点和用法
  • Render PropsHOC React Hooks相比,有哪些优劣(重要面试题)

HOC高阶组件

高阶组件(HOC):是React中用于复用组件逻辑的一种高级技巧HOC自身不是React API的一部分,它是一种基于React的组合特性而形成的设计模式

高阶组件可以看做React装饰器模式的一种实现,具体而言,高阶组件是参数作为组件,返回值为新组件的函数

HOC解决的问题

  • 抽离公共组件,实现组件代码复用,常见场景:页面复用。
  • 条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景:权限控制。
  • 捕获/劫持被处理组件的生命周期,常见场景:组件渲染性能追踪、日志打点。

当我们项目中使用高阶组件开发时,能够让代码变得更加优雅,同时增强代码的复用性和灵活性,提升开发效率

高阶组件的基本框架

高阶组件的框架:

export default (WrappedComponent) => {
 return class NewComponent extends React.Component {
  // 可以自定义逻辑
    // 比如给 WrappedComponent组件传递props和methods
    render () {
   return <WrappedComponent {...this.props}/>
    }
  }
}

如果自定义了statemethods可以通过下面方式传递到子组件中

export default (WrappedComponent) => {
 return class NewComponent extends React.Component {
  state = {
      markTimenew Date().toLocaleTimeString(); // 获取组件当前渲染时的时间
    }
    printTime() {
      let myDate = new Date();
      let myTime= myDate.toLocaleTimeString(); 
      console.log('当前时间', myTime)
    }
    render () {
   return <WrappedComponent markTime={this.state.markTime} printTime={this.printTime}/>
    }
  }
}

这样在WrappedComponent组件中,如果是类组件就可以通过this.props.markTime获取,函数组件的话通过props.markTime来获取,方法获取和状态获取相同。

HOC可以做什么

属性代理——可操作所有传入的props

可以读取、添加、编辑、删除传给 WrappedComponent 的 props(属性)

「场景描述」:Hello组件传递show,hide方法,让其显示Loading加载框

const loading = message => OldComponent => {
  return class extends React.Component {
    // 显示一个 Loading的div
    state = {
      show() => {
        let div = document.createElement('div');
        div.innerHTML = `<p id="loading" style="position: absolute; z-index:10; top: 10; color: red; border: 1px solid #000">${message}</p>`
        document.body.appendChild(div);
      },
      hide() => {
        document.getElementById('loading').remove();
      }
    }
    render() {
      return (
        <div>
          <OldComponent {...this.props} {...this.state}/>
        </div>
      )
    }
  }
}
function Hello(props) {
  return (
    <div>hello
      <button onClick={props.show}>show</button>
      <button onClick={props.hide}>hide</button>
    </div>
  )
}

let HightLoadingHello = loading('正在加载')(Hello);
ReactDom.render(<HightLoadingHello/>, document.getElementById('root'));

效果如图:

抽离公共组件,最大化实现复用

「场景描述」: 统计每个组件的渲染时间

class CalTimeComponent extends React.Component {
  componentWillMount() {
    this.start = Date.now(); // 初始渲染节点
  }
  componentDidMount() {
    console.log((Date.now() - this.start) + 'ms');
  }
  render() {
    return <div>calTimeComponent</div>
  }
}

ReactDom.render(<CalTimeComponent/>document.getElementById('root'));

这样仅仅能计算当前组件的渲染时间,假如现在有这样一个需求,需要统计每个组件的渲染时间呢?

就应该想到把它抽离出去,比如:

// CalTimeComponent.js
export default function CalTimeComponent(OldComponent{
  return class extends React.Component {
    state = {
      markTimenew Date().toLocaleTimeString()
    }
    componentWillMount() {
      this.start = Date.now();
    }
    componentDidMount() {
      console.log((Date.now() - this.start) + 'ms');
    }
    printTime() {
      let myDate = new Date();
      let myTime= myDate.toLocaleTimeString(); 
      console.log('当前时间', myTime)
    }
    render() {
      return <OldComponent markTime={this.state.markTime} printTime={this.printTime}/>
    }
  }
}

// HelloComponent.js
import withTracker from '../../Components/CalTimeComponent.js';

class HelloComponent extends React.Component{
  render() {
    console.log(this.props);
    this.props.printTime()
    return <div>hello</div>
  }
}

let HighHelloComponent = CalTimeComponent(HelloComponent);
ReactDom.render(<HighHelloComponent/>, document.getElementById('root'));

这样就能最大化的实现CalTimeComponent组件复用了,把它引入到想要计算时间的组件里,并传入当前组件就好了。

Render Props

特点1render props指在一种React组件之间使用一个值为函数的props共享代码的简单技术。

特点2:具有render props的组件接收一个函数,该函数返回一个React元素并调用它而不是实现一个自己的渲染逻辑。

特点3render props是一个用于告知组件需要渲染什么内容的函数(props)

特点4:也是组件逻辑复用的一种实现方式

接下来,我通过一个例子带大家分别认识上面的四种特点

「场景描述」: 在多个组件内实时获取鼠标的x、y坐标

原生实现:不复用逻辑

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      x0,
      y0,
    }
  }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }
  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        <h1>请移动鼠标</h1>
        <p>当前鼠标的位置是:x:{this.state.x} y:{this.state.y}</p>
      </div>

    )
  }
}

ReactDom.render(<MouseTracker/>document.getElementById('root'));

上面,这是在一个组件内完成的,假如现在要在多个div内完成上面的逻辑该怎么办,就该想到复用了,看看render prop是怎么帮我们完成的?

Render Props

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      x0,
      y0,
    }
  }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }
  render() {
    console.log(this.props)
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>

    )
  }
}

ReactDom.render(
<MouseTracker render={
  props =>
 (
    <React.Fragment>
      <h1>请移动鼠标</h1>
      <p>当前鼠标的位置是: x:{props.x} y:{props.y}</p>
    </React.Fragment>
  )
}></MouseTracker>
document.getElementById('root'));

注意:render props 是因为模式才被称为 render props ,你不一定要用名为 renderprops 来使用这种模式。render props 是一个用于告知组件需要渲染什么内容的函数 `prop

那如果改写成高阶组件呢?

高阶组件写法

改写成高阶组件,并将公共组件抽离出去, ShowPosition子组件中可以拿到withTracker父组件中传递的x、y坐标值

// withTracker.js
export default function withTracker (OldComponent{
  return class MouseTracker extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        x0,
        y0,
      }
    }
    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY
      });
    }
    render() {
      console.log(this.props)
      return (
        <div onMouseMove={this.handleMouseMove}>
          <OldComponent {...this.state}/>
        </div>
      )
    }
  }
}

// ShowPosition.js
import withTracker from '../../Components/withTracker.js';

function ShowPosition(props) {
  return (
    <React.Fragment>
      <h1>请移动鼠标</h1>
      <p>当前鼠标的位置是: x:{props.x} y:{props.y}</p>
    </React.Fragment>
  )
}

// 在 ShowPosition 组件中 可以拿到 withTracker 传递过来的坐标值
let HightShowPosition = withTracker(ShowPosition);

ReactDom.render(<HightShowPosition/>, document.getElementById('root'));

hoc、render props、react-hooks的优劣如何?

HOC的优势:

  • 抽离公共组件,实现组件代码复用,常见场景:页面复用。
  • 条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景:权限控制。
  • 捕获/劫持被处理组件的生命周期,常见场景:组件渲染性能追踪、日志打点。
  • 属性代理,可以给一些子组件传递层次比较远的属性,并按需求操作他们

HOC的缺陷:

  • 扩展性限制HOC无法从外部访问子组件(被包裹组件 WrappedComponent)的 state,因此无法通过shouldComponentUpdate过滤掉不必要的更新(React支持ES6之后,提供了 React.pureComponent来解决这个问题)
  • Ref传递问题Ref由于组件被高阶组件包裹,导致被隔断,需要后来的 React.forwardRef来解决这个问题
  • 层级嵌套HOC可能出现多层包裹组件的情况(一般不超过两层,否则不好维护)多层抽象增加了复杂度和理解成本
  • 命名冲突:如果高阶组件多次嵌套,没有使用 命名空间的话会产生冲突,覆盖老属性

Render Props优点:

  • 上述HOC的缺点,Render Props都可以解决

Render Props缺陷:

  • 使用繁琐HOC只需要借助装饰器/高阶函数的特点就可以进行复用,而 Render Props需要借助 回调嵌套
  • 嵌套过深Render Props虽然摆脱了组件多层嵌套的问题,但是转化为了 函数回调的嵌套

React Hooks的优点(重点):

  • 简洁React Hooks解决了 HOCRender Props的嵌套问题,更加简洁
  • 解耦React Hooks可以更方便地把 UI 和状态分离,做到更彻底的解耦
  • 无影响复用组件逻辑Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • 函数友好:React Hooks为函数组件而生,从而解决了类组件的几大问题
    • this 指向容易错误
    • 分割在不同声明周期中的逻辑使得代码难以理解和维护
    • 代码复用成本高(高阶组件容易使代码量剧增)

React Hooks的缺陷(重点):

  • 额外的学习成本(Functional Component 与 Class Component 之间的困惑)
  • 写法上有限制(不能出现在条件、循环中,只能在最外层调用 Hooks),并且写法限制增加了重构成本
  • 破坏了 PureComponent、React.memo浅比较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)(依赖项不变,可解决该问题)
  • 使用不当,可能会造成 闭包陷阱问题
  • React.memo并不能完全替代 shouldComponentUpdate(因为拿不到 state change,只针对 props change)

关于react-hooks的评价来源于官方react-hooks RFC

感谢大家

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「 在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
  2. 欢迎加我微信「 Augenstern9728」一起交流学习...
  3. 关注公众号「 前端时光屋」,持续为你推送精选好文。

本文分享自微信公众号 - 前端时光屋(javascriptlab)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部