参考:
mobx官方文档:https://mobx.js.org/react-integration.html
1. 了解mobx收集依赖的机制
MobX 采用"运行时依赖追踪"策略,其核心规则可概括为:只有在当前执行上下文中实际被读取的 observable 数据才会建立依赖关系。(使用在function中的数据也不会被收集)
对于 React 组件而言,这种依赖收集主要发生在组件的 render 阶段。
1.1 render中引用的observable数据才会被收集
🌰:component组件无法监听子组件中render的数据变更。
这个问题主要在component组件中。(FC组件中数据在当前执行上下文里的)
有这种场景:页面Index组件使用的是component组件,其中有很多的子组件,子组件中有一个状态a,父组件Index需要监听状态a的变化,回调其他的方法。
然后就会有一个问题:状态a变动时,componentDidUpdate不会被触发。
是因为状态a没有收集到页面组件Index的依赖。
这种情况使用reaction解决即可,但是反应出来的mobx自动收集依赖的机制需要了解。
1.2 最小粒度的引用,精准地更新
mobx的依赖收集以最小粒度为准。
比如:
你可以尝试一下,点击按钮修改user.email
不会触发重渲染,修改user.age
哪怕没有在return中也会触发重渲染,修改user.name
同样。
const user = observable({
name: 'test',
age: 1,
email: 'xxx',
});
const UserFC = observer(() => {
// ✅ 被解构里面实际使用的字段才会建立依赖,变动响应
// ❌ 如果其他地方改变了 user.email 字段,不会触发更新
const { name, age } = user;
console.log('重新渲染了');
return (
<Button onClick={action(() => (user.email = '22222'))}>
{name}
</Button>
);
});
1.3 利用收集机制优化性能,减少重渲染次数
还是上面的例子,建议父组件传递observable数据不必细致到最小粒度,下方组件Child需要渲染user.name
,但是父组件传的是user
,而不是user.name
。
如果点击Button,会发现Child渲染了,但是Parent不会重渲染,这样节省了一个父组件的渲染性能。
const user = observable({
name: 'test',
age: 1,
email: 'xxx',
});
const Child = observer(({user})=>{
console.log('Child渲染',user.name)
return <div>{JSON.stringify(user.name)}</div>
})
const UserFC = observer(() => {
console.log('Parent渲染');
return (
<Button onClick={action(() => (user.name = '22222'))}>
<Child user={user} />
</Button>
);
});
2. 状态修改通过 @action
设置configure({ enforceActions: "always" });
保证在除了action以外的其他地方不允许直接修改store的值。
2.1 使用runInAction合并多个action操作
runInAction
可以用来合并多个 action 操作,确保这些操作在同一个事务中执行,避免不必要的中间渲染。
推荐在需要修改多个可观察属性的场景使用,可以避免每次属性变更时触发的多次重渲染。
2.2 action操作对象遵循纯函数规则,使用不可变数据
即遵循redux中的reducer思路。纯函数规则和使用不可变数据是函数式编程的良好实践,能让代码更加可预测和易于维护。
如果我们直接写在action里面,就不用每次都拷贝一次了。
对于reducer的实现处理有如下建议:
- 简单对象和不复杂的数据结构:使用展开运算符 {…}即可。
- 复杂对象、深层嵌套的对象的不可变处理,推荐使用immer,能够带来书写时更好的体验。
import {produce} from 'immer';
@action
addTodo = (text) => {
this.todos = produce(this.todos, draft => {
draft.push({ test:111 });
});
}
3. 使用多种不同的@observable监听方式
参考:https://mobx.js.org/observable-state.html
@observable 、@observable.deep
描述:
深度递归比对
observable.ref
描述:
观察对象是否被替换,而不会深度监听对象内部属性的变化。
*适用于不可变数据,只有对象的引用地址变更才会触发监听。
@observable.ref
test={
shallow_test:1,
shallow_obj:{
test1:{test2:'11'}
}
}
observable.struct
描述:
比较新旧值的结构是否相同(用于深度不可变场景)
*不能用在大型对象上,使用 observable.struct 会导致对比耗时
*可用于解决相同数据重复渲染的问题
使用:
和ref有点像,修改内部的值它是不会变的,只有对象的引用地址变更+内部数据有变化才会触发监听。
⬇️ 比如下面的例子,因为地址变更但是内部数据不变,所以不会触发组件重渲染。
@observable.struct
test={
shallow_test:1,
shallow_obj:{
test1:{test2:'11'}
}
}
@action
setTest = () => {
this.test = {
shallow_test:1,
shallow_obj:{
test1:{test2:'11'}
}
}
};
observable.shallow
描述:
只监听对象第一层属性(类似浅层监听)
使用:
比如下面的对象,只有shallow_test、shallow_obj变化时才会触发监听,shallow_obj.test1不会触发监听。
@observable.shallow
test={
shallow_test:1,
shallow_obj:{
test1:{test2:'11'}
}
}
observable.array
专为array设计,在引用地址不变的情况下,push / pop也可以触发变化监听
observable.map
专为对象设计,在引用地址不变的情况下修改内部值,也可以触发变化监听
4. 使用@computed自动计算
mobx会把store中的getter自动转为@computed,使用@computed的场景主要在多数据依赖、需要缓存的时候,会比较方便。
5. 💡监听特定数据使用reaction、autorun、when
请遵循以下原则:
- 监听数据并且做业务动作,使用reaction。(即:reaction和autorun都能用的时候选择reaction)
- debug需要跟踪值变化时可以使用autorun。
- when用于监听到某个条件时触发。
- 监听完毕后需要清除监听。
// autorun
// 自动收集observable数据的变动。
const dispose = autorun(() => {console.log(store.count)};
dispose();
// reaction
const dispose = reaction(
() => store.count,
(newCount) => {console.log(newCount);
});
dispose()
// when
const dispose = when(
() => store.count === 3,
() => {console.log('等于3时触发');
});
dispose()
6. mobx-react中可以使用mobx-react-lite的方法
扩展帮助:https://juejin.cn/post/6844904147922190349
注:根据mobx-react-lite的readme,useObserver、useLocalStore、useAsObservableSource、ObserverBatching都已经弃用。
useLocalObservable
在组件内创建一个简易的Store状态管理,以替代useState、useEfffect,(Mobx可自动收集依赖,无需像useEffect那样需要手动输入),使用get属性时mobx将自动转化为computed。
- 注意:我们的组件一般用不到这么复杂的状态管理工具,从功能上来说和React提供的工具方法有重合,并且自动收集依赖项而不是自己控制可能存在风险。
observer
将一个组件转化为可观察组件,mobx将自动收集组件内的变化数据,将此组件加入响应式的依赖内。
observer自动应用memo,因此observer组件永远不需要包裹在memo内。
import { observer } from "mobx-react-lite" // Or "mobx-react".
const MyComponent = observer(props => ReactElement)
Observer
mobx的github索引:<Observer />
。
其实也是创建了一个observer包裹的组件。
❗️但是不确定匿名函数的创建是否会在外层组件重渲染时重新创建,导致不必要的组件开销。
const TodoView = observer(({ todo }: { todo: Todo }) => {
// CORRECT: wrap the callback rendering in Observer to be able to detect changes.
return <GridRow onRender={() => <Observer>{() => <td>{todo.title}</td>}</Observer>} />
})
7. eslint规则:eslint-plugin-mobx
参考网址:https://www.npmjs.com/package/eslint-plugin-mobx
扩展问题:
1. observer包裹导出的高阶组件部分rules-of-hooks无法生效
const Child = Parent(() => {
if (window.test === 1) return;
// 不会触发eslint:react-hooks/rules-of-hooks报错
useEffect(() => {}, []);
return (
<div>
<TopBar />
</div>
);
});
const Parent = (Component) => {
return <Component />;
};
参考:
- eslint-plugin-react-hooks的代码:https://github.com/facebook/react/blob/e1378902bbb322aa1fe1953780f4b2b5f80d26b1/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
- 类似issue:
- https://github.com/facebook/react/issues/26072
- https://github.com/facebook/react/pull/27546
原因:
eslint认可的高阶匿名函数组件只有Reaact官方提供的forwardRef和memo。
我们自定义的高阶组件和mobx的observer 包装后的组件都无法被Eslint认为是一个函数组件。
解决方案
- 推荐:使用非匿名函数:
function Component (){}
// ✅ Child先定义好名称,再传入MobxWrap中。
const Component = () => { ... }
export const DecoratedComponent = observer(Component);
// ❌ 会引发eslint: prefer-arrow-callback错误
const _FC = MobxWrap(['app'],function Index(){
// 会触发 prefer-arrow-callback 报错
})
- 函数式
// 使用hook+Observer
const Child = (props:{test:number})=>{
const {app} = useMobx(['app'])
if(window.test === 1) return;
useEffect(()=>{
},[])
return <Observer>{()=>(<div />)}</Observer>
}
- 使用eslint认可的方法(forwardRef、memo)包裹,使eslint可识别这是一个组件