看完你还不懂Mobx和Redux来找我

1,416 阅读7分钟

一、什么是Mobx

在中文官网上是这么介绍的:

MobX 是一个身经百战的库,它通过运用透明的函数式响应编程使状态管理变得简单和可扩展。 通过这句话可以知道Mobx的一个特点就是响应式编程,对于响应式,Mobx以一种类似于Vue的形式实现的,所以其需要用 observer 函数去收集依赖,一个简单的数据流图如下:

image.png

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,当计算属性所依赖的发生变化时,就会重新计算该计算属性的值,让后将更新前后的值进行对比,如果更新前后的值没发生变化就会使用之前缓存的值。

这里有两个问题:

  1. 所依赖的值具体是指什么?

首先我们要明白,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 函数。详细示例请点击这里

  1. 更新前后的值采用什么方式进行对比?

更新前后的值对比要分两种情况,如果这个值是一个原始数据类型就直接调用 === 进行比较就行,如果是引用数据类型,那么默认请况下会比较他们的引用。详细解释点击这里

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,最终输出如下:

image.png

前三个输出是首次渲染的结果,后面两个输出是触发 Click 事件后的输出,我们会发现从 Child1 组件开始到 App组件都会重新 render 一遍,至于 Child2 为什么没变是因为使用了 memo 进行优化,同时更新前后的 props.state 的引用是没有变的。

而对于 Mobx 来说他的数据天生就是 Mutable 的,也就是说除非我们手动把属性赋值一个新的值或引用,不然这个引用始终不会改变,正是由于这个特性 Mobx 才能实现更细粒度的更新。

2、异步action

我们知道 Redux 本身是不支持异步 action 的,如果要实现异步能力,必须引入redux-thunkredux-saga编写而外的代码,而 Mobx 是原生就支持异步 action 的,比如下面这个例子:

class Store {
  constructor() {
    makeAutoObservable(this);
  }

  async fetchProjects() {
    try {
      const projects = await fetchGithubProjectsSomehow();
      runInAction(() => {
       //...
      });
    } catch (e) {
      runInAction(() => {
        //...
      });
    }
  }
}

3、其他

  1. Redux支持时间回溯可以增强业务的可预测性,而 Mobx是没有时间回溯的,因为数据始终只有一份。
  2. 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 处理待定的数据。