一、什么是Mobx
在中文官网上是这么介绍的:
MobX 是一个身经百战的库,它通过运用透明的函数式响应编程使状态管理变得简单和可扩展。 通过这句话可以知道Mobx的一个特点就是响应式编程,对于响应式,Mobx以一种类似于Vue的形式实现的,所以其需要用 observer 函数去收集依赖,一个简单的数据流图如下:
Mobx 使用的是单向数据流,通过触发 action 去更新可响应的对象,继而去更新计算属性并触发副作用。
二、Mobx的使用
1、响应式对象
Mobx通过 makeObservable/makeAutoObservable 方法来构造响应式对象,对象中的属性会通过 Proxy 代理,这和 Vue 相似。
class Todo {
count = 0;
data = {
name: "aaa",
age: 12,
};
constructor() {
makeObservable(this, {
count: observable,
data: observable,
setData: action
})
}
setData(name) {
this.data.name = name;
}
}
在 makeObservable 函数中需要去指明每个属性的类型,如果嫌麻烦可以用 makeAutoObservable 去替代这种写法,他是 makeObservable 的加强版,能够自动为属性加上对应的类型。
2、计算属性
Mobx 的计算属性和 Vue 的 computed API 一样,在 makeObservable 中将 getter 声明为 computed,当计算属性所依赖的值发生变化时,就会重新计算该计算属性的值,让后将更新前后的值进行对比,如果更新前后的值没发生变化就会使用之前缓存的值。
这里有两个问题:
- 所依赖的值具体是指什么?
首先我们要明白,Mobx 是跟踪属性的访问,而不是属性的值 下面通过官一个例子来说明:
class Message {
author = {
name: "mobx",
age: 24,
};
constructor() {
makeAutoObservable(this);
autorun(() => {
console.log(this.author);
});
autorun(() => {
console.log(this.author.name);
});
}
updateAuthorName(name) {
this.author.name = name;
}
updateAuthor(author) {
this.author = author;
}
}
const message = new Message();
setTimeout(() => {
console.log("第一个setTimeout执行了~");
message.updateAuthorName("mobx1");
}, 1000);
setTimeout(() => {
console.log("第二个setTimeout执行了~");
message.updateAuthor({
name: "mobx2",
age: 25,
});
}, 3000);
在这个例子中我们有两个 autorun 函数,第一个函数依赖 author 对象,第二个依赖 author 对象中的 name 属性,再定义两个定时器先改变 author 对象的 name属性,再去改变整个 author 对象,我们先执行下查看下结果。
//{ name: [Getter/Setter], age: [Getter/Setter] }
//mobx
//第一个setTimeout执行了~
//mobx1
//第二个setTimeout执行了~
//{ name: [Getter/Setter], age: [Getter/Setter] }
//mobx2
前两个输出就不用解释了,1s后执行第一个setTimeout的回调改变 name 的值,发现此时只打印出了 mobx1,3s后执行第二个回调,此时 author 和 author.name 都打印出来了,这是因为第一个 autorun 函数依赖的是 author 这一个可观察对象,只有当这个可观察对象本身发生改变之后 (this.author = newAuthor )才会调用这个 autorun 函数。详细示例请点击这里
- 更新前后的值采用什么方式进行对比?
更新前后的值对比要分两种情况,如果这个值是一个原始数据类型就直接调用 === 进行比较就行,如果是引用数据类型,那么默认请况下会比较他们的引用。详细解释点击这里
3、监听变更
当数据发生改变时,如果我们想去做一些额外的操作,那就需要用到一些专门用来监听数据改变的 API,比如 autorun、reaction、when,这些 API 可以理解为 React Hooks 中的 useEffect,当依赖的属性发生改变时会调用他的回调函数。
class Todo {
count = 0;
id = 1;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
}
const todo = new Todo();
//当count改变时就会执行
autorun(() => {
console.log(`autorun ${todo.count}`);
});
//当count改变时就会执行,id改变时是不会执行的
reaction(
() => todo.count,
//第一个参数为当前值,第二个参数为修改前的值
(value, preValue) => {
console.log(`reaction ${value} ${todo.id}`);
}
);
//当第一个方法返回true时就会执行,这个方法只会执行一次
when(
() => todo.count > 5,
() => {
console.log(`when ${todo.count}`);
}
);
setInterval(() => {
todo.increment();
}, 1000);
三、Mobx VS Redux
redux 和 Mobx 从宏观上来说都是一个状态管理库,用来解决状态管理混乱、无法有效同步的问题,因此他们也有着许多相似的概念:统一维护应用状态、只能通过特定方式去修改状态等,下面主要是从几个方面去介绍两者的不同点。
1、数据可变性
在数据可变性上 Mobx 使用的是可变(Mutable)的数据即可以通过原生的js方式去修改属性的值,而 Redux 使用的是不可变(Immutable)的数据,也就是说在 Redux中我们不能直接操作状态对象,而是必须在原来的状态对象上返回一个新的状态对象,也就是俗称的 ...,然而这样的话就会导致送你从你想要修改的那个数据的引用开始,到他的父 object 引入,一直到根 object 的引用都会发生变化,变成一个新的引用,那么在这条路径下的所有组件都会更新。
//reducer
const init = {
info: {
id: 1,
name: "xxx",
},
state: {
id: 2,
name: "aaa",
},
};
// eslint-disable-next-line import/no-anonymous-default-export
export default function (state = init, action) {
switch (action.type) {
case "changeInfo":
return { ...state, info: { ...state.info, name: action.name } };
case "changeState":
return { ...state, state: { ...state.state, name: action.name } };
default:
return state;
}
}
//App.js
const Child1 = ({ info }) => {
console.log("Child1");
return (
<div>
<p>{info.name}</p>
<p>{info.id}</p>
</div>
);
};
const Child2 = memo(({ state }) => {
console.log("Child2");
return (
<div>
<p>{state.name}</p>
<p>{state.id}</p>
</div>
);
});
function App() {
const data = useSelector((state) => state);
const dispatch = useDispatch();
console.log("App");
return (
<div
className="App"
onClick={() => {
dispatch({ type: "changeInfo", name: "pppp" });
}}
>
<Child1 info={data.info} />
<Child2 state={data.state} />
</div>
);
}
在这里我们创建了一个 store,里面的 info 和 state 属性都是一个对象,分别通过 props 的形式传给 Child1 和 Child2 组件,此时我们触发 Click 事件去改变 name,最终输出如下:
前三个输出是首次渲染的结果,后面两个输出是触发 Click 事件后的输出,我们会发现从 Child1 组件开始到 App组件都会重新 render 一遍,至于 Child2 为什么没变是因为使用了 memo 进行优化,同时更新前后的 props.state 的引用是没有变的。
而对于 Mobx 来说他的数据天生就是 Mutable 的,也就是说除非我们手动把属性赋值一个新的值或引用,不然这个引用始终不会改变,正是由于这个特性 Mobx 才能实现更细粒度的更新。
2、异步action
我们知道 Redux 本身是不支持异步 action 的,如果要实现异步能力,必须引入redux-thunk或redux-saga编写而外的代码,而 Mobx 是原生就支持异步 action 的,比如下面这个例子:
class Store {
constructor() {
makeAutoObservable(this);
}
async fetchProjects() {
try {
const projects = await fetchGithubProjectsSomehow();
runInAction(() => {
//...
});
} catch (e) {
runInAction(() => {
//...
});
}
}
}
3、其他
- Redux支持时间回溯可以增强业务的可预测性,而 Mobx是没有时间回溯的,因为数据始终只有一份。
- Redux样板代码过长,而 Mobx 结构非常简洁
四、recoil VS redux
这里只将 recoil 和 redux 进行对比是因为他们都是基于Immutable 的数据流管理方案,同时在设计上也有许多想通之处,在基本的功能上 recoil 可以覆盖 redux,下面罗列的是他们的主要区别。
1、数据的定义
我们知道 redux 采用的是集中式定义 state 的方式,将需要共享的数据全部定义在一个 store 里面,这样带来的问题在上面我们有过介绍,就是会导致组件不必需要的渲染,因此 Recoil 就采用分散管理源自状态的设计模式,将数据的定义分散开来。
const State = atom({
key: "TodoState",
default: [],
});
这里的 key 必须在 RecoilRoot 作用于内唯一。
2、数据的修改
在 Redux 中修改数据是通过纯函数来实现的,我们通过 dispatch 触发对应的 action 将数据更新到 state 中,而 Recoil 采用的是 Hook 的形式,这有点类似我们 React 中的 useState 的第二个返回值。
function App() {
const setTodoState = useSetRecoilState(TodoState);
setTodoState(() => {
//...
})
}
3、派生值(selector)
派生值这个概念与 Mobx 的 computed 类似,selector 就是通过 atom 定义出来的一种复合状态,它会根据依赖的 atom计算得到对应的值,并将这个值进行缓存,在下次获取的时候如果依赖的值没有变化就可以直接从缓存中取值。
const filteredTodoListState = selector({
key: "filteredTodoListState",
get: ({ get }) => {
const list = get(TodoState);
return list.filter((item) => item.isComplete);
},
});
在 selector 中也是支持异步获取数据的,此时我们只需将get 变成一个异步的函数就行。
const filteredTodoListState = selector({
key: "filteredTodoListState",
get: async({ get }) => {
const state = get(State);
const response = await fetchData({
id: state.id
})
return response.data;
},
});
因为我们 React 的渲染函数是同步的,所以这里可以配合React Suspense 处理待定的数据。