关于React高级特性

170 阅读5分钟

React高级

函数组件对比class组件的区别

  • 纯函数,输入props,输出JSX
  • 没有实例,没有生命周期,没有state
  • 不能扩展其他方法

适用场景:一些简单页面,接收props,返回jsx

非受控组件

前面基础篇讨论过受控组件主要处理表单数据,但是可能有一些更复杂的逻辑需要我们获取到组件的DOM节点,进行一些处理,就显得麻烦,比如我们的文件上传组件富文本等,这时我们就可以使用非受控组件来获取组件处理。来看看实际的例子:

import React, { Component } from 'react';

export default class UncontrolledDemo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: 'airhua'
    }

    // 创建ref
    this.nameInput = React.createRef()
    this.fileInputRef = React.createRef()
  }

  consoleFile = () => {
    const elem = this.fileInputRef.current
    console.log(elem.files[0].name);
  }

  render() {
    return <div>
      {/* 场景一般是需要操控dom */}
      <input type="text" ref={this.state.nameInput} defaultValue={this.state.name} />
      <input type="file" ref={this.fileInputRef} />
      <button onClick={this.consoleFile}>打印文件名</button>
    </div>;
  }
}

ref

首先我们先在constructor中创建好了我们的ref对象:

this.nameInput = React.createRef()
this.fileInputRef = React.createRef()

defaultValue默认值

在标签中我们使用ref等于state中定义的ref进行实例化,还可以看到相比受控组件我们多出了defaultValue,这里只是一个初始值,后面的DOM更新值后这个值并不会随之更新。

<input type="text" ref={this.state.nameInput} defaultValue={this.state.name} />
<input type="file" ref={this.fileInputRef} />

current

最后我们可以通过ref中current拿到DOM实例

consoleFile = () => {
  const elem = this.fileInputRef.current
  console.log(elem.files[0].name);
}

因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

Portal

创建 portal。Portal 将提供一种将子节点渲染到 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。

来看看例子:

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

export default class ProtalsDemo extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    return ReactDOM.createPortal(
      <div className="modal">{ this.props.children }</div>,
      document.body
    )
  }
}

上面便是一个简单的Protal创建过程。

1644397636452.png

可以看到我们定义的组件并没有渲染到root节点内,这里还不得不说一下this.props.children,你可以把它和vue中slot联系一下,它接收组件里面的内容传到到这。

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

import React, { Component } from 'react'

const ThemeContext = React.createContext('light')

function TooBar() {
  return (
    <ThemeButton></ThemeButton>
  )
}

class ThemeButton extends Component {
  // 指定contextType读取当前的theme context
  static contextType = ThemeContext

  render() {
    // 读取到了之后就可以使用 this.context使用
    const theme = this.context
    return <div>
      <p>当前主题 {theme}</p>
    </div>
  }
}

export default class ContentDemo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: 'light'
    }
  }

  changeTheme = () => {
    this.setState({
      theme: this.state.theme === 'light' ? 'dark' : 'light'
    })
  }

  render() {
    // 使用一个 Provider 将当前theme传递给以下组件树
    // 无论多深,任何组件都能读取这个值
    return (
      <ThemeContext.Provider value={ this.state.theme }>
        <TooBar></TooBar>
        <button onClick={this.changeTheme}>改变主题</button>
      </ThemeContext.Provider>
    )
  }
}

异步组件

React.lazy+React.Suspense

实现组件的异步加载,在组件未加载出来,可以添加等待加载组件。

import React, { Component } from 'react'

// 引入异步组件
const ContextDemo = React.lazy(() => import('./ContextDemo'))

export default class LazyDemo extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    return (
      <div>
        <p>异步组件</p>
        <React.Suspense
          // 在异步组件未加载出来之前显示的loading...
          fallback={<div>loading...</div>}
        >
          <ContextDemo></ContextDemo>
        </React.Suspense>
      </div>
    )
  }
}

React性能优化相关

shouldComponentUpdate

我们在下面写了一个例子,父组件包含两个子组件,当其中一个子组件更新时,另一个子组件数据并未改变但是却还是触发了componentDidUpdate方法,这里我们就可以写shouldComponentUpdate解决这个问题。

import React, { Component } from 'react'

class List extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    return (
      <div>
        {
          this.props.list.map((item, index) => (
            <li key={index}>{ item }</li>
          ))
        }
      </div>
    )
  }
}

class Footer extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  // 内容未变, 但是重复渲染
  componentDidUpdate() {
    console.log('componentDidUpdate')
  }

  // 返回boolean类型 决定更新
  shouldComponentUpdate(nextProps, nextState) {
    if (nextProps.text === this.props.text) {
      return false
    } else {
      return true
    }
  }

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

export default class SCU extends Component {
  constructor(props) {
    super(props)
    this.state = {
      list: [10, 20, 30],
      text: 'footer'
    }
  }

  add = () => {
    this.setState({
      list: this.state.list.concat(60)
    })
  }

  render() {
    return (
      <div>
        <List list={this.state.list}></List>
        {/* 这里执行方法改变state */}
        <button onClick={this.add}>增加</button>
        <Footer text={this.state.text}></Footer>
      </div>
    )
  }
}

PureComponent

你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。

比如上面Footer组件可以改写成:

class Footer extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {}
  }

  // 不再执行
  componentDidUpdate() {
    console.log('componentDidUpdate')
  }

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

做这个性能提升还可以用React.memo实现,这里就不过多演示,可以访问链接了解

包括对不可变值的写法,可以通过immutable.js实现约束

组件公共逻辑的抽离

你可以想象,在一个大型应用程序中,一个逻辑可能在多个组件中重复实现,我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。以下是两个抽离公共逻辑的方式。

高阶组件(HOC)

高阶组件是参数为组件,返回值为新组件的函数。

HOC 在 React 的第三方库中很常见,例如 Redux 的 connect 和 Relay 的 createFragmentContainer

下面我们来实现一个自己的HOC函数,例子为打印一个鼠标坐标:

import React, { Component } from 'react'

// 高阶组件
const withMouse = (Component) => {
  class withMouseComponent extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        x: 0,
        y: 0
      }
    }

    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY
      })
    }

    render() {
      return (
        <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
          {/* 包裹传入组件,在外层实现公共逻辑,...this.props穿透让返回组件保留原来的props */}
          <Component {...this.props} mouse={this.state}></Component>
        </div>
      )
    }
  }

  return withMouseComponent
}


class HOCDemo extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    const { x, y } = this.props.mouse
    return (
      <div>
        <h1>鼠标位置: {x}, { y }</h1>
      </div>
    )
  }
}

// 暴露的是withMouse返回的组件
export default withMouse(HOCDemo)

Render-props实现

核心是把公共逻辑封装函数作为一个props传入子组件

import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Mouse extends Component {
  // render函数必须传入
  static propTypes = {
    render: PropTypes.func.isRequired
  }

  constructor(props) {
    super(props)
    this.state = {
      x: 0,
      y: 0
    }
  }

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }

  render() {
    return (
      <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
        {/* 执行传入函数 */}
        {this.props.render(this.state)}
      </div>
    )
  }
}

export default class RenderPropDemo extends Component {
  render() {
    return (
      <Mouse
        // 传入函数 作为props
        render={
          ({ x, y }) => <h1>坐标: {x}, { y }</h1>
        }
      ></Mouse>
    )
  }
}

HashRouter和BrowserRouter区别

HashRouter

  • 基于hash模式,页面跳转原理使用location.hash、location replace
  • 适用于To B项目,部署内网,本公司业务人员使用的项目等

BrowserRouter

  • 基于history模式,页面跳转原理是使用了history对象api,history.pushState、history.replaceState
  • 适用于To C项目、面向大众项目,需要后端做处理

使用方法

import React from 'react'
import {
  // HashRouter as Router, // hash模式
  BrowserRouter as Router, // history模式
  Route,
  Switch,
} from 'react-router-dom'
import Home from '../containers/home'

const BasicRouter = () => (
  <Router>
    <Switch>
      <Route exact path="/" component={Home} />
    </Switch>
  </Router>
)

export default BasicRouter