react的context更新而组件不更新的解决方案

代码地址请在github查看,也欢迎您star,issue,共同进步!

1.react中父组件的shouldComponentUpdate返回false

在React的Context API中,如果context中属性发生变化,而父组件的shouldComponentUpdate返回false,那么该父组件中所有子组件都不会监测到conext的变化,从而不会reRender。比如下面的例子中,我们的ThemeProvider中的context发生了变化,但是TodoList的属性props没有发生改变,所以TodoList的shouldComponentUpdate返回了false,那么TodoList下面产生的所有的ThemedText的颜色都不会发生变化了(没有检测到ThemeProvider的contex变化)。请点击demo

class ThemeProvider extends React.Component {
 //Will invoke when state or props change!
  getChildContext() {
    return {color: this.props.color}
  }
  render() {
    return <div>{this.props.children}</div> } } //Add color props to context ThemeProvider.childContextTypes = { color: React.PropTypes.string }

上面这个ThemeProvider 组件用于将props中获取到的color属性放置到react的context中。

class ThemedText extends React.Component {
  render() {
    return <div style={{color: this.context.color}}> {this.props.children} </div> } } //Will get color prop from parent Component context ThemedText.contextTypes = { color: React.PropTypes.string }

ThemedText 组件负责从父级组件的context中获取color来作为自己的style。

class TodoList extends React.PureComponent {
  render() {
    return (<ul> {this.props.todos.map(todo => <li key={todo}><ThemedText>{todo}</ThemedText></li> )} </ul>) } }

TodoList 组件用于从props中获取到todos属性,同时根据这些属性来渲染出我们的ThemedText组件。下面给出我们最顶层的组件:

const TODOS = ["Get coffee", "Eat cookies"]
class App extends React.Component {
  constructor(p, c) {
    super(p, c)
    this.state = { color: "blue" } 
  }
  render() {
    return (
         {/*ThemeProvider将color放到context中*/}
         <ThemeProvider color={this.state.color}>
              <button onClick={this.makeRed.bind(this)}> {/*ThemedText通过this.context.color获取到父组件的color属性设置style*/} <ThemedText>Red please!</ThemedText> </button> <TodoList todos={TODOS} /> {/* (1)当点击按钮的时候,按钮本身颜色发生变化,但是TodoList这个组件没有变化,因为它继承了React.PureComponent,所以组件的props没有发生变化,那么组件不会重新渲染。因此,我们的TodoList里面的所有的组件颜色都不会发生变化,因为TodoList组件的shouldComponentUpdate返回了false!!!!因此不管是TodoList还是他的子组件都不会更新 (2)更加糟糕的是,我们也不能在TodoList中手动实shouldComponentUpdate,因为SCU根本就不会接收到context数据(只有当它的state或者props修改了才能接收到,getChildContext才会调用)。 (3)因此,shouldComponentUpdate如果返回false,那么所有的子组件都不会接收到context的更新。如这里的TodoList组件返回了false,那么TodoList下所有的ThemedText从context中获取到的color都不会更新,这很重要!!!! */} </ThemeProvider> ) } //rewrite color attr in state makeRed() { this.setState({ color: "red" }) } } ReactDOM.render( <App />, document.getElementById("container") )
2.使用依赖注入(context-based Dependency Injection)

使用依赖注入(context-based Dependency Injection)来解决当ThemeProvider的context发生变化,同时TodoList的shouldComponentUpdate返回false(其Props发生了变化),而子组件不会更新的情况。其原理其实就是:不再将context作为一个容器,而是作为一个事件系统。请点击demo。此时ThemeProvider变成了如下内容:

class ThemeProvider extends React.Component {
  constructor(s, c) {
    super(s, c)
    this.theme = new Theme(this.props.color)
  }
  /** * 这个方法组件在shouldComponentUpdate之前肯定是被调用的,所以我们的这个内置的Theme对象肯定可以接收到下一个状态的color */
  componentWillReceiveProps(next) {
    this.theme.setColor(next.color)
  }
  //传入到子组件中的是theme而不是我们的color属性
  getChildContext() {
    return {theme: this.theme}
  }
  render() {
    return <div>{this.props.children}</div> } } //放到子组件中的是theme ThemeProvider.childContextTypes = { theme: React.PropTypes.object }

我们看看我们的Theme事件系统:

class Theme {
  // this.theme = new Theme(this.props.color)
  constructor(color) {
    this.color = color
    this.subscriptions = []
  }

 /* *每次setColor的时候将我们的subscription中的所有的函数都调用一遍 */
  setColor(color) {
    this.color = color
    this.subscriptions.forEach(f => f())
  }
  /** * 调用subscribe方法来push一个函数用于执行 */
  subscribe(f) {
    this.subscriptions.push(f)
  }
}

此时我们传递到下级组件的往往是一个Theme对象而不是一个color属性,即传递的不再是state的容器,而是一个小型的事件系统。我们再看看TodoList下的ThemedText如何在TodoList的shouldComponentUpdate返回false的情况下(props没有发生改变)进行了组件更新。

class ThemedText extends React.Component {
    /** * 在所有子组件中,我们在componentDidMount中我们会获取到theme然后注册我们的事件,并且强制组件更新 */
  componentDidMount() {
    this.context.theme.subscribe(() => this.forceUpdate())
  }
  render() {
    return <div style={{color: this.context.theme.color}}> {this.props.children} </div> } } ThemedText.contextTypes = { theme: React.PropTypes.object }

其实我们的顶级组件压根就不需要进行改造,和上一种情况完全一致:

class App extends React.Component {
  constructor(p, c) {
    super(p, c)
    this.state = { color: "blue" } 
  }

  render() {
    return (
           <ThemeProvider color={this.state.color}> {/* (1)ThemeProvider将color这个props放到context中,不过不是直接放进去,而是采用一种更加优雅的方式来完成 (2)在ThemeProvider中传递给子组件的不再是color,而是一个依赖注入系统然后所有需要接收到Context更新的组件全部subscribe这个事件即可 (3)TodoList我们依然使用的是React.PureComponent,但是其下面的ThemedText组件却可以接收到我们的ThemeProvider的context更新(前提TodoList本身prop/state没有改变) (4)这个事件系统的实现有点简单,我们需要在componentWillUnMount中取消 事件监听,同时应该使用setState而不是forceUpdate让组件更新 */} <button onClick={this.makeRed.bind(this)}> <ThemedText>Red please!</ThemedText> </button> <TodoList todos={TODOS} /> </ThemeProvider> ) } makeRed() { this.setState({ color: "red" }) } }

参考资料:
How to safely use React context
Why Not To Use Context

你可能感兴趣的:(react)