ReactCollect icon indicating copy to clipboard operation
ReactCollect copied to clipboard

关于在mobx中如何observe观察深层的数据变动

Open ckinmind opened this issue 7 years ago • 13 comments

背景:

发现在mobx中似乎深层次的数据变动有时候不会触发新的render, 例子如下:

// 在store中
class SpaceStore {
    @observable treeData = {
              id: 100,
              name: 'test',
              children: []
      };
}


//在组件中
const store = new SpcaceStore();
class Space extends Component {


     componentDidMount(){
        console.log('componentDidMount')
        store.treeData.name = 'hehe';
     }
  ...
  render(){
    console.log('this is render');

    //第一种情况,打印整个treeData(需要注释第二种情况)
    console.log(store.treeData)

   //第二种情况,打印treeData中的name(需要注释注释第一中情况)
     console.log(store.treeData.name)

  //第三种,解除前两条的注释

    return (
        <div>test</div>
    );
  }
}
  1. 第一种情况(第二种情况是注释的),打印出的内容以及顺序如下
this is render
Object
componentDidMount

可以奇怪的地方在于,打印的Object代表是treeData,这是在name改变前打印的,可是奇怪的是其name值已经变成了'hehe'

  1. 第二种情况(注释第一种情况),打印的内容以及顺序如下:
this is render
test
componentDidMount
this is render
hehe

仅仅是打印store.treeData.name, 居然能触发第二次render, 并且打印的name值前后变化都是正确的

  1. 解除前两条的注释,即如下
...
 console.log('this is render');
 console.log(store.treeData)
 console.log(store.treeData.name)
...

打印的结果以及顺序如下:

this is render
test
Object(其中name指为hehe)
componentDidMount
this is render
Object(其中name值为hehe)
hehe

奇怪的地方在于两次打印的treeData值中的name值都是改变后,这好奇怪

ckinmind avatar Mar 14 '17 12:03 ckinmind

@ckinmind Hi, 我搜索mobx深层变动时无意中看到你这里。首先谢谢你的这些链接,我顺着搜索到很多有用的信息。

1.我认为你上面所说的情况应该是正确的,比如第一种情况

可以奇怪的地方在于,打印的Object代表是treeData,这是在name改变前打印的,可是奇怪的是其name值已经变成了'hehe'

打印的object只是引用:当你在console里面看(读取)这个object,虽然是先打印出来的,但其实它只是个引用,指向的是被改动了的object,所以你看到的是新的name,这其实和mobx没关系,而是因为js里面object是ref(引用)

2.而我在Mobx深层数据变动遇到的问题是添加新的attribute进object,这个新的attribute却不被观察,比如

@observable treeData = {}

@action addNewAttr () {
 this.treeData['name'] = 'new added'
}

我的react component却没有因为这个addNewAttr的action触发任何的render事件...也就是说这一行为并没有被观察

最后我用了 map 这个数据结构 https://mobx.js.org/refguide/map.html , 这是我其中一个store

import { observable, action, map } from 'mobx';
import catalogAPI from '../api/catalog';

class Store {
  @observable data = map({}) // {catalogId: catalogObject}

  @action findById(id) {
    return catalogAPI.findById(id).then(catalog => {
      // 使用merge添加新的数据,会触发react里面的render
      this.data.merge({ [catalog.id]: catalog });
      return catalog;
    })
  }
}

export default new Store();

3.最后还有个问题,我看到你store export出去的都是个store class,而我的store都是export 这个store的instance出去,我的目的在于,这样在我的SPA里面,我的store状态就都是共享的了。虽然之前看别人文档也大多数export 这个store class出去,但我现在用下来还是觉得export store的instance比较适合SPA,我也是第一次尝试mobx,不知道你有没有什么想法在这方面?

wwayne avatar Mar 24 '17 08:03 wwayne

@wwayne 你好,首先很抱歉,其实我很早就看到了你的回复,因为之前太忙,加上有些问题我自己还没有思考清楚,所以没有及时回复,现在乘着清明节放假,我谈一些我的思考

  1. 确实是引用类型的原因,我原来以为先打印的话会打印改变前的状态,这并非遵循时间顺序上变化

  2. 关于深层次数据变动无法触发更新的问题,原因应该是出在原数据是observable的,但是其深层次的子数据不是observable的,拿map类型举例,可以参考我这个issue #107

    对于这种问题,我个人有一个比较取巧的解决方案,你应该发现了如果在一个action中改变了多个observable的变量,但是最终只会触发一次render,在这一次render中将改变全部反映到视图上,所以,对于复杂类型的数据,我们可以额外定义一个updateKey(observable),当每次需要更新视图的时候,改变这个updateKey的值就行,使用了这种方式之后,其他的变量我都不需要定义observable了,代码如下


@observer
 class Test extends Component {

data = [];   // data内存储的是嵌套很深的数据
@observable updateKey = '';  //定义一个用于触发更新的变量

/** 视图触发器,里面什么也不用做 */
@action renderTrigger = () => {
 };

@action changeData = () => {
     // 异步或者别的什么操作,改变了data

    this.updateKey = Math.random();  // 随机改变updateKey的值,只要和之前不一样就行
    // 因为这里改变了,所以触发了renderTrigger的依赖变动(即使里面什么也不做),
   // 从而触发了视图更新, 从而将改变都反应到视图中
};

   render(){

    this.renderTrigger(this.updateKey);  // 这里必须传入updateKey,效果类似autorun

    return (
      .......
    )
  } 
}
  1. 任何需要改变data然后视图更新的地方,都可以通过改变updateaKey来触发视图更新
  2. 极端情况是,只定义一个updateKey是observable,其他所有的变量都可以不用是observable,任何数据改变后需要触发更新,只要直接this.updateKey = Math.random(); 就行
  3. 还有一个额外的好处是,可以让data的数据结构变得清晰,否则使用observable之后结构看起来就不太一样,加上可能还需要转换状态比如toJS,slice之类的,使用了我这种方式就可以直接使用原数据
  1. 确实是export 出去实例的好,我之前export store,然后在最顶级组件中实例化,然后如果子组件需要就通过prop传过去, 后来我看到这篇文章使用Mobx更好地处理React数据, 发现可以直接export 一个实例出去,然后哪里需要就import进去就行,这也引发了我一个新的疑问就是export出去的是共享一个实例还是每次import 都是一个新的store,自己测试的结果是共享一个

希望我的回复能对你有价值

ckinmind avatar Apr 04 '17 11:04 ckinmind

@ckinmind 感谢 :) 确实第二点如你所说,你这样的话data的数据结构会清楚很多,确实不失为一个有用的小技巧。也许封装下会更好,否则我觉得可能会坑队友,哈哈哈。 再次感谢你的认真回复,收获良多 :)

wwayne avatar Apr 04 '17 12:04 wwayne

上面的讨论很实用。 updateKey 这种方式曾在项目中实践过,但控制更新的视图范围更大,如果 observe 所有 data 的话,如果视图更零散,控制更新的视图会更精细。

huyansheng3 avatar Feb 09 '18 07:02 huyansheng3

非常棒的讨论,感谢!

shopshow avatar Jul 17 '18 10:07 shopshow

问下现在有更好的解决方案了吗

leeseean avatar Sep 20 '18 01:09 leeseean

@leeseean 过了这么久了,还是觉得updateKey这总方式最好

ckinmind avatar Sep 28 '18 07:09 ckinmind

学习了,感谢!

zxiaohong avatar Apr 10 '19 03:04 zxiaohong

发现这种方式可以触发更新Object.assign({}, this.loading, { [key]: status }),下面是我的代码片段

import { observable, action } from 'mobx'

class FetchLoadingStore {
  @observable loading = {}

  @action setLoading(key, status) {
    this.loading = Object.assign({}, this.loading, { [key]: status })
  }
}

const fetchLoadingStore = new FetchLoadingStore()
export { fetchLoadingStore }

lvSally avatar Nov 01 '19 02:11 lvSally

@ckinmind ,学习了,我封装了一下

import { observable, action } from "mobx";

class RenderTrigger {
  @observable
  key: number;

  @action
  reRender() {
    this.key = Math.random();
  }

  watch(key: number) {}
}

export const renderTrigger = new RenderTrigger();

在数据结构处:

udpate() {
    renderTrigger.reRender();
}

在组件里:

render() {
    renderTrigger.watch(renderTrigger.key);
    return (
        <div>...
    )
}

讨论已经过去两年多,不知道有没有更好的办法。

haanamomo avatar Apr 07 '20 02:04 haanamomo

每次更新用lodash的cloneDeep重新赋值

linshuizhaoying avatar Sep 14 '20 08:09 linshuizhaoying

@linshuizhaoying 可以考虑对于复杂的数据结构,用更新另外一个observable变量的方式触发更新的方式来降低复杂度,代码可能会更加简单清爽

ckinmind avatar Sep 18 '20 06:09 ckinmind