gmfe/Think

记mobx的一个问题

Realwate opened this issue · 4 comments

问题

以下代码可在这里查看。

const store = observable({
  name: 'My Name',
});
// 假设Container 是第三方组件
function Container(props){
    return <div>
        <h1> Container </h1>
        {props.render()}
      </div>
}

// App.js
@observer
class App extends React.Component {
  renderSome() {
    return <div>
        { store.name }
    </div>
  }

  @action
  changeName(){
      // 用于修改mobx store的action
      store.name = Date.now();
  }
  render() {
    return <div>
      <Container render={this.renderSome} />
      <button onClick={this.changeName}> Change </button>
    </div>
  };
}

App.js是我们的业务代码,它会调用一个第三方组件ContainerContainer接受一个render props用于自定义渲染内容,render props中会访问mobxstore
Change按钮用于修改store.name,但是点击却 不会重新渲染组件

原因

使用了observer装饰的组件,它的render会被包装,在 render函数执行时 根据它访问的observable数据,记录下依赖。一旦依赖的observable变化,就会调用forceUpdate更新组件。
但是在父子组件情况下,React的生命周期相关函数执行顺序如下:

/**
 * ------------------ The Life-Cycle of a Composite Component ------------------
 *
 * - constructor: Initialization of state. The instance is now retained.
 *   - componentWillMount
 *   - render
 *   - [children's constructors] 
 *     - [children's componentWillMount and render] 
 *     - [children's componentDidMount] 
 *     - componentDidMount
 *
 *       Update Phases:
 *       - componentWillReceiveProps (only called if parent updated)
 *       - shouldComponentUpdate
 *         - componentWillUpdate
 *           - render
 *           - [children's constructors or receive props phases]
 *         - componentDidUpdate
 *
 *     - componentWillUnmount
 *     - [children's componentWillUnmount]
 *   - [children destroyed]
 * - (destroyed): The instance is now blank, released by React and ready for GC.
 *
 * -----------------------------------------------------------------------------
 */

App组件是被observer装饰过的,由以上顺序可知,App组件的render执行时 不会调用到Containerrender ,因此renderSome中访问store.name,实际上不会被App组件收集到依赖。

解决

错误的做法(可略过)

一开始我是这样做的,将Apprender略作改动。

// App.js
@observer
class App extends React.Component {
  /* ** */
  render() {
      // 获取一次name 触发依赖收集
    let name = store.name;
    return <div>
      <Container render={this.renderSome} />
      <button onClick={this.changeName}> Change </button>
    </div>
  };
}

但是并没有彻底解决问题,仍然有以下不足

  1. 『可读性』。App render中并没有使用name,后面阅读代码的人会产生疑惑。
  2. 『代码规范』。如果使用了ESlintname属于no used valuefix时可能被干掉。
  3. 『性能』。依赖name的其实是Container组件(内部的一部分),这种做法使得只要name变化就会重新render整个App,如果App组件树稍微复杂,就会带来很多不必要的render

最佳方案

其实官方文档早已对这种情况做出了说明(多读文档的重要性)。直接看解决方法,代码在这里

// App.js
import { observer,Observer } from "mobx-react";
// 0. 
// Container = observer(Container);
@observer
class App extends React.Component {
  renderSome() {
   // 修改renderSome。以下两种写法都可以
   // 1. 使用oberver函数包装
   const SomeComponent = observer(() =>
       <div>
           { store.name }
       </div>)

   return <SomeComponent/>
     
    // 2. Oberver组件形式,接受一个render props
   /*
   return  <Observer>
       {
         ()=> <div>
           { store.name }
       </div>
       }
   </Observer>
   */

  }
}

以上三种方案,第0种直接用observer装饰Container,可行,但是数据变化的同时,也会重新render整个Container组件。
第1,2种是官方推荐的方式,将我们访问Observable数据的地方视为一个组件(函数), 使用observer装饰即可。如果不想额外创建组件,可以使用Observer组件形式。
这两种方案使得store.name变化时,只会render相关的这一个小组件,不会产生多余的render,是最优解。

有 React的生命周期相关函数执行顺序 的引用地址么,想看更多

文中引用的顺序出自React15.0版本源码中的注释 地址

相关文章有:
深入理解React16之:(一).Fiber架构
React v16.3之后的组件生命周期函数

66666,提个建议,这个名称可以改一下“记mobx的一个问题”这个标题感觉不是很清晰

补充一个资料: mobx何时作出响应