React高级指南之上下文

826 阅读5分钟

路漫漫其修远兮,吾将上下而求索。— 屈原《离骚》

写在前面

Context提供了一种在组件树传递数据的方法,而无需在每一层传递props属性

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

我们先来通过一个例子来看看我们不使用Context的情况

查看DEMO

欢迎star

先上效果图

我们通过select来切换我们的主题,每次改变后三个部分的主题背景颜色都发生的改变

下面附上核心代码部分

class Header extends Component {
  render() {
    const { theme, children } = this.props 
    return (
      <header className={theme}>
        { children }
      </header>
    )
  }
}

class ThemeHeader extends Component {
  render() {
    const { theme, children } = this.props 
    return (
      <Header theme={theme}>
        { children }
      </Header>
    )
  }
}

...

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: 'none' // dark light none
    }
    this.handleChange = this.handleChange.bind(this)
  }
  handleChange(e) {
    this.setState({
      theme: e.currentTarget.value
    })
  }
  render() {
    const { theme } = this.state
    const { handleChange } = this
    return (
      <React.Fragment>
        <ThemeHeader theme={theme}>
          <label for='theme'>切换主题:</label>
          <select name='theme' value={theme} onChange={handleChange}>
            <option value='dark'>dark</option>
            <option value='light'>light</option>
            <option value='none'>none</option>
          </select>
        </ThemeHeader>
        <ThemeAside theme={theme}>侧边栏</ThemeAside>
        <ThemeMain theme={theme}>主体</ThemeMain>
      </React.Fragment>
    )
  }
}

代码看一遍就知道是啥意思,我也不过多解释。主要是这里在每个UI组件外面又套了一层Theme,主要是为了传递theme属性,试想一下如果我们的组件结构很深,我们就必须创建很多不必要的组件来传递属性,Context上下文就是帮我们解决这个问题,可以让多个组件共享某些数据

查看DEMO

API

createContext

React 16提供了一个createContext方法来创建上下文对象,这个方法接收一个参数作为默认值,并返回一个Context对象,该对象有两个属性Provide和Consumer

const { Provider, Consumer } = React.createContext({
  theme: 'dark'
})

Provider和Consumer可以当作组件来使用

Provider

可以被当作组件来使用

<Provider value={/* some value */}>

接收一个value属性并传递给Consumer,一个Provider里面可以包含多个Consumer,并且Provider可以被嵌套

Consumer

可以被当作组件来使用

<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

需要接收一个函数作为子节点,输入参数为离当前Consumer最近的Provider的value属性,如果找不到Provider,则为createContext中的输入参数

注意当Provider的value属性发生变化,该Provider里层的Consumer都会重新渲染,注意这个重新渲染不受shouldComponentUpdate的影响,即使shouldComponentUpdate返回false,但是Provider的value发生了改变,属于该Provider的Consumer也会重新渲染

比较Provider的value的值是否发生改变是通过 Object.is

应用

Context的应用其实还是挺多了,下面举几个例子帮助大家再好好理解

结合高阶组件

前面React高级指南之高阶组件这篇文章已经详细介绍了什么是高阶组件及其应用,我们还可以将高阶组件和Contex进行结合

代码附上

import React, { Component } from 'react'
import './App.css'

const { Provider, Consumer } = React.createContext({
  theme: 'none'
})

const getDisplayName = component => {
  return component.displayName || component.name || 'Component'
}

const withTheme = WrappedComponent => {
  return class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <Consumer>
          {
            value => <WrappedComponent {...this.props} {...value}>{this.props.children}</WrappedComponent>
          }
        </Consumer>
      )
    }
  }
}

@withTheme
class Header extends Component {
  render() {
    const { theme, handleChange } = this.props 
    return (
      <header className={theme}>
        <label for='theme'>切换主题:</label>
        <select name='theme' value={theme} onChange={handleChange}>
          <option value='dark'>dark</option>
          <option value='light'>light</option>
          <option value='none'>none</option>
        </select>
      </header>
    )
  }
}

@withTheme
class Aside extends Component {
  render() {
    const { theme } = this.props
    return (
      <aside className={theme}>
        侧边栏
      </aside>
    )
  }
}

@withTheme
class Main extends Component {
  render() {
    const { theme } = this.props
    return (
      <main className={theme}>
        主体
      </main>
    )
  }
}

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: 'light' // dark light none
    }
    this.handleChange = this.handleChange.bind(this)
  }
  handleChange(e) {
    this.setState({
      theme: e.currentTarget.value
    })
  }
  render() {
    const { theme } = this.state
    const { handleChange } = this
    return (
      <Provider value={{theme}}>
        <Header handleChange={handleChange}/>
        <Aside />
        <Main />
      </Provider>
    )
  }
}

export default App

上面将高阶组件,Context,修饰器三者结合,灵活运用

转发Ref

React高级指南之高阶组件一章中我们已经介绍了ref不会被传递,这个是因为ref不是一个真正的属性,React 对它进行了特殊处理,当时我们是通过_ref来传递ref,今天我们来介绍另外一种方法-转发Ref,我们需要使用React.forwardRef

关于这个函数的用法我后面会专门写一篇文章介绍,在这里我们只需要知道

React.forwardRef((props, ref) => (
  <CustomComponent />
));

利用转发Ref我们给上述的三个组件Header,Aside,Main添加ref属性

const withTheme = WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <Consumer>
          {
            value => <WrappedComponent {...this.props} {...value} ref={this.props.forwardRef}>{this.props.children}</WrappedComponent>
          }
        </Consumer>
      )
    }
  }
  return React.forwardRef((props, ref) => (
    <HOC {...props} forwardRef={ref} />
  ))
}

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: 'light' // dark light none
    }
    this.handleChange = this.handleChange.bind(this)
    this.headerRef = React.createRef()
    this.asideRef = React.createRef()
    this.mainRef = React.createRef()
  }
  handleChange(e) {
    this.setState({
      theme: e.currentTarget.value
    })
  }
  render() {
    const { theme } = this.state
    const { handleChange, headerRef, asideRef, mainRef } = this
    return (
      <Provider value={{theme}}>
        <Header handleChange={handleChange} ref={headerRef}/>
        <Aside ref={asideRef}/>
        <Main ref={mainRef}/>
      </Provider>
    )
  }
}

打开React Devtool看下组件树的结构:

每个被包裹的组件上面都有了Ref属性了(之前这个属性是绑定在HOC上的)

点我预览

查看完整代码

手动实现Provider

之前通过一个计数器来了解react-redux及其用法这篇文章已经手动实现了Provider,今天我们继续将再次手动实现,也算对以前的知识做一个回顾,我们将直接使用redux,而不使用react-redux

直接上代码:

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import './index.css'

const theme = (state = 'light', action) => {
  switch (action.type) {
    case 'CHANGE_THEME':
      return action.theme
    default:
      return state
  }
}
const reducer = combineReducers({theme})

const store = createStore(reducer)

const StoreContext = React.createContext({
  store
})

const getDisplayName = component => {
  return component.displayName || component.name || 'Component'
}

const withTheme = WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    constructor(props) {
      super(props)
      const {store} = this.props
      this.state = store.getState()
    }
    render() {
      const {store, forwardRef} = this.props
      let newProps = {
        dispatch: store.dispatch
      }
      Object.keys(this.props).filter(key => key !== 'store' && key !== 'forwardRef' && key !== 'children').forEach(item => {
        newProps[item] = this.props[item]
      })
      return (
        <WrappedComponent {...newProps} {...this.state} ref={forwardRef}></WrappedComponent>
      )
    }
    componentDidMount() {
      const {store} = this.props
      store.subscribe(() => {
        this.setState(store.getState)
      })
    }
  }
  return React.forwardRef((props, ref) => (
    <HOC {...props} forwardRef={ref}/>
  ))
}

@withTheme
class Header extends Component {
  render() {
    const {dispatch, theme} = this.props
    return (
      <header className={theme}>
        <label htmlFor='theme'>切换主题:</label>
        <select name='theme' value={theme} onChange={e => {dispatch({type: 'CHANGE_THEME', theme: e.currentTarget.value})}}>
          <option value='dark'>dark</option>
          <option value='light'>light</option>
          <option value='none'>none</option>
        </select>
      </header>
    )
  }
}

@withTheme
class Aside extends Component {
  render() {
    const {theme} = this.props
    return (
      <aside className={theme}>
        侧边栏
      </aside>
    )
  }
}

@withTheme
class Main extends Component {
  render() {
    const {theme} = this.props
    return (
      <main className={theme}>
        主体
      </main>
    )
  }
}

class Provider extends Component {
  render() {
    const {children, store} = this.props
    return (
      <StoreContext.Provider value={store}>
        <StoreContext.Consumer>
          {
            store => React.Children.map(children, (child, index, arr) => {
              return React.cloneElement(child, {store}, null)
            })
          }
        </StoreContext.Consumer>
      </StoreContext.Provider>
    )
  }
}

ReactDOM.render(
<Provider store={store}>
  <Header ref={React.createRef()}/>
  <Aside ref={React.createRef()}/>
  <Main ref={React.createRef()}/>
</Provider>, document.getElementById('root'))

上面这段代码将状态使用redux进行管理,创建一个Provider组件用来接收全局的store对象,Provider的内部使用的Context上下文提供的Provider和Consumer组件,Context的Provider将store传递给Context的Consumer。Context.Consumer的内部接收一个函数作为子节点,然后对Provider的子节点进行遍历,对每个item使用React.cloneElement方法(在每个item上将外部的store传递进去),而每个item又是一个高阶组件,高阶组件的内部订阅了state的变化,高阶组件将theme属性传递给被包裹组件,当我们改变了theme时会发起一个action,store接收到action 会重新计算state,这时候在订阅的回调函数里面会调用setState方法,后面的过程就是组件生命周期的过程了。(表述的可能不太好,大家理解代码的思想就可以了)

点我预览

查看完整代码

最后

本文只是简单对Context的用法去做了介绍,大家可以在以后的工作和学习中多积累其用法,也可以去看看React-Redux中Provider的实现,其实就是借助Context来让所有组件都能获取到store

React学习之路很有很长

你们的打赏是我写作的动力

微信
支付宝