ReactCollect
ReactCollect copied to clipboard
关于在mobx中如何observe观察深层的数据变动
背景:
发现在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>
);
}
}
- 第一种情况(第二种情况是注释的),打印出的内容以及顺序如下
this is render
Object
componentDidMount
可以奇怪的地方在于,打印的Object代表是treeData,这是在name改变前打印的,可是奇怪的是其name值已经变成了'hehe'
- 第二种情况(注释第一种情况),打印的内容以及顺序如下:
this is render
test
componentDidMount
this is render
hehe
仅仅是打印store.treeData.name, 居然能触发第二次render, 并且打印的name值前后变化都是正确的
- 解除前两条的注释,即如下
...
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 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 你好,首先很抱歉,其实我很早就看到了你的回复,因为之前太忙,加上有些问题我自己还没有思考清楚,所以没有及时回复,现在乘着清明节放假,我谈一些我的思考
-
确实是引用类型的原因,我原来以为先打印的话会打印改变前的状态,这并非遵循时间顺序上变化
-
关于深层次数据变动无法触发更新的问题,原因应该是出在原数据是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 (
.......
)
}
}
- 任何需要改变data然后视图更新的地方,都可以通过改变updateaKey来触发视图更新
- 极端情况是,只定义一个updateKey是observable,其他所有的变量都可以不用是observable,任何数据改变后需要触发更新,只要直接
this.updateKey = Math.random();
就行- 还有一个额外的好处是,可以让data的数据结构变得清晰,否则使用observable之后结构看起来就不太一样,加上可能还需要转换状态比如toJS,slice之类的,使用了我这种方式就可以直接使用原数据
- 确实是export 出去实例的好,我之前export store,然后在最顶级组件中实例化,然后如果子组件需要就通过prop传过去, 后来我看到这篇文章使用Mobx更好地处理React数据, 发现可以直接export 一个实例出去,然后哪里需要就import进去就行,这也引发了我一个新的疑问就是export出去的是共享一个实例还是每次import 都是一个新的store,自己测试的结果是共享一个
希望我的回复能对你有价值
@ckinmind 感谢 :) 确实第二点如你所说,你这样的话data的数据结构会清楚很多,确实不失为一个有用的小技巧。也许封装下会更好,否则我觉得可能会坑队友,哈哈哈。 再次感谢你的认真回复,收获良多 :)
上面的讨论很实用。 updateKey 这种方式曾在项目中实践过,但控制更新的视图范围更大,如果 observe 所有 data 的话,如果视图更零散,控制更新的视图会更精细。
非常棒的讨论,感谢!
问下现在有更好的解决方案了吗
@leeseean 过了这么久了,还是觉得updateKey这总方式最好
学习了,感谢!
发现这种方式可以触发更新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 }
@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>...
)
}
讨论已经过去两年多,不知道有没有更好的办法。
每次更新用lodash的cloneDeep重新赋值
@linshuizhaoying 可以考虑对于复杂的数据结构,用更新另外一个observable变量的方式触发更新的方式来降低复杂度,代码可能会更加简单清爽