Mobx

649 阅读7分钟

简介

Mobx 是一个简单,可拓展的状态管理库。React 和 Mobx 是一对强力的组合,React 负责渲染应用, Mobx 负责应用状态的管理,提供给React使用

版本支持情况

  • 目前最新版本为 6,版本 4 和版本 5 已不再支持
  • 在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法
  • MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node
  • MobX 通常和 React 配合使用,但是在 AngularVue 中也可以使用 MobX。

Mobx 入门

下载

  • mobx:MobX 核心库
  • mobx-react-lite:仅支持函数组件
  • mobx-react:既支持函数组件也支持类组件

核心概念

  1. observable state:被 MobX 跟踪的状态。
  2. action:允许修改状态的方法,在严格模式下只有 action 方法被允许修改状态。
  3. computed:根据应用程序状态派生的新值,计算值

工作流程

image.png

入门案例

场景描述: 计数器:在组件中显示数值状态,单击按钮使数值加一,单击按钮使数值重置。

// Counter Component -> Counter 组件
// observer: 监控当前组件使用到的由 MobX 跟踪的 observable state, 当状态发生变化时通知 React 更新视图
import { observer } from "mobx-react-lite"

function Counter({ counterStore }) {
  return (
    <div>
      <p className="paragraph">{counterStore.count}</p>
      <button onClick={() => counterStore.increment()} className="button">加 1</button>
      <button onClick={() => counterStore.reset()} className="button">重置</button>
    </div>
  )
}

export default observer(Counter)
// Counter Store -> 管理 Counter 组件的 Store
import { makeAutoObservable } from "mobx"

class CounterStore {
  // 数值状态
  count = 10
  constructor() {
    // 将参数对象中的属性设置为 observable state
    // 将参数对象中的方法设置为 action
    makeAutoObservable(this)
  }
	// 使数值状态加一
  increment() {
    this.count += 1
  }
	// 重置数值状态
  reset() {
    this.count = 0
  }
}

export default CounterStore
// App Component -> 根组件
// 导入 Counter 组件
import Counter from "./components/Counter/Counter"
// 导入管理 Counter 组件的 Store
import CounterStore from "./stores/Counter/CounterStore"
// 创建管理 Counter 组件的 Store 实例对象
const counterStore = new CounterStore()

function App() {
  // 调用 Counter 组件并传入管理其状态的 Store
  return <Counter counterStore={counterStore} />
}

export default App

makeAutoObservable

// target: 将目标对象中的属性和方法设置为 observable state 和 action
// overrides: 覆盖默认设置, 将 target 对象中的某些属性或者方法设置为普通属性
// options: 配置对象, autoBind, 使 action 方法始终拥有正确的 this 指向
makeAutoObservable(target, overrides?, options?)
makeAutoObservable(this, {reset: false}, {autoBind: true})

总结

状态变化更新视图的必要条件

  1. 状态需要被标记为 observable state
  2. 更改状态的方法需要被标记为 action 方法
  3. 组件视图必须通过 observer 方法包裹

可以使用 makeAutoObservable 方法将对象属性设置为 observable state,将对象方法设置为 action 方法

可以使用 observer 方法监控当前组件使用到的由 MobX 跟踪的 observable state,当状态发生变化时通知 React 更新视图

TodoList 案例

image.png

创建初始 TodoListStore

创建 TodoListStore,用于管理待办事项列表状态。

// TodoListStore.js
class TodoListStore {
  todos = []
  constructor(todos) {
    if (todos) this.todos = todos
  }
}
export default TodoListStore

创建 TodoViewStore,用于管理待办事项状态。

// TodoViewStore.js
class TodoViewStore {
  id = Math.random()
  title = ""
  completed = false
  constructor(title) {
    this.title = title
  }
}
export default TodoViewStore

创建 TodoListStore 实例对象并将其传入到 TodoListView 组件

// App.js
import TodoListView from "./components/Todos/TodoListView"
import TodoListStore from "./stores/Todos/TodoListStore"
import TodoStore from "./stores/Todos/TodoStore"

const counterStore = new CounterStore()

const todoListStore = new TodoListStore([
  new TodoStore("Hello MobX"),
  new TodoStore("Hello React")
])

function App() {
  return <TodoListView TodoListStore={todoListStore} />
}

渲染初始待办事件

// TodoListView.js
function TodoListView({ TodoListStore }) {
  return (
    <ul className="todo-list">
      {TodoListStore.todos.map(todo => (
        <TodoView key={todo.id} todo={todo} />
      ))}
    </ul>
  )
}

// TodoView.js
function TodoView({ todo }) {
  return <label>{todo.title}</label>
}

创建待办事项

  1. 在 TodoListStore 类中创建 createTodo 方法,用于向 todos 数组中添加待办事项
// TodoListStore.js
import TodoStore from "./TodoStore"

class TodoListStore {
  createTodo(title) {
    this.todos.push(new TodoStore(title))
  }
}
  1. 在 TodoHeader 组件中调用 createTodo 方法创建待办事项
// TodoListView.js
function TodoListView({ TodoListStore }) {
  return <TodoHeader createTodo={title => TodoListStore.createTodo(title)} />
}
// TodoHeader.js
import { useState } from "react"

function TodoHeader({ createTodo }) {
  const [title, setTitle] = useState("")
  return (
    <header className="header">
      <input
        value={title}
        onChange={event => setTitle(event.target.value)}
        onKeyUp={event => {
          if (event.key === "Enter") {
            createTodo(title)
            setTitle("")
          }
        }}
      />
    </header>
  )
}
  1. 设置状态变化更新视图的必要条件
// TodoListStore.js
import { action, makeObservable, observable } from "mobx"

class TodoListStore {
  constructor() {
    makeObservable(this, {
      todos: observable,
      createTodo: action
    })
  }
}

// TodoListView.js
import { observer } from "mobx-react-lite"

function TodoListView (){}

export default observer(TodoListView)

创建Store上下文

我们希望在每个组件中都能够直接获取到 TodoListStore,而不是通过 props 属性传递的方式。

// TodoListStore.js
import { createContext, useContext } from "react"

const TodoListStoreContext = createContext()

const TodoListStoreProvider = ({ store, children }) => {
  return (
    <TodoListStoreContext.Provider value={store}>
      {children}
    </TodoListStoreContext.Provider>
  )
}

const useTodoStore = () => {
  return useContext(TodoListStoreContext)
}

export { TodoListStore, TodoListStoreProvider, useTodoStore }

// App.js
import { TodoListStore, TodoListStoreProvider } from "./stores/Todos/TodoListStore"

function App() {
  return (
    <TodoListStoreProvider store={todoListStore}>
      <TodoListView/>
      <Counter counterStore={counterStore} />
    </TodoListStoreProvider>
  )
}
// TodoListView.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"

function TodoListView() {
  const todoListStore = useTodoListStore()
}

// TodoHeader.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"

function TodoHeader() {
  const todoListStore = useTodoListStore()
  todoListStore.createTodo(title)
}

删除待办事件

// TodoListStore.js
class TodoListStore {
  constructor() {
    makeObservable(this, {
      removeTodo: action
    })
  }
  removeTodo(id) {
    const index = this.todos.findIndex(todo => todo.id === id)
    this.todos.splice(index, 1)
  }
}
// TodoView.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"

function TodoView({ todo }) {
  const todoListStore = useTodoListStore()
  return <button onClick={() => todoListStore.removeTodo(todo.id)} className="destroy" />
}

更改任务状态

// TodoStore.js
import { makeObservable, observable, action } from "mobx"

class TodoStore {
  completed = false
  constructor() {
    makeObservable(this, {
      completed: observable,
      toggle: action
    })
  }
  toggle() {
    this.completed = !this.completed
  }
}
// TodoView.js
import { observer } from "mobx-react-lite"

function TodoView({ todo }) {
  return (
    <li className={todo.completed ? "completed" : ""}>
      <input checked={todo.completed} onChange={() => todo.toggle()} className="toggle" type="checkbox" />
    </li>
  )
}

export default observer(TodoView)

更正this指向

<input onChange={() => todo.toggle()} />
<input onChange={todo.toggle}/> // this 指向会发生错误

makeObservable(this, {
  toggle: action.bound
})

计算待办事项数量

待办事项数量属于派生状态,即该状态依赖现有状态 (todos) 生成。

派生状态可以使用计算值实现,当依赖状态发生变化后,计算值自动更新。

// TodoListStore.js
import { computed } from "mobx"

class TodoListStore {
  constructor() {
    makeObservable(this, {
      unCompletedTodoCount: computed
    })
  }
  get unCompletedTodoCount() {
    return this.todos.filter(todo => !todo.completed).length
  }
}


// TodoFooter.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
import { observer } from "mobx-react-lite"

function TodoFooter() {
  const todoListStore = useTodoListStore()
  return <strong>{todoListStore.unCompletedTodoCount}</strong> item left
}

export default observer(TodoFooter)

注意:计算值是被缓存的。


get unCompletedTodoCount() {
  console.log("unCompletedTodoCount")
  return this.todos.filter(todo => !todo.completed).length
}
{todoListStore.unCompletedTodoCount}
{todoListStore.unCompletedTodoCount}
{todoListStore.unCompletedTodoCount}
// 计算属性被调用多次, 但是方法内部的console.log 只会输出一次, 说明计算属性是被缓存的.

待办事项过滤

// TodoListStore.js
class TodoListStore {
  todos = []
  filter = "all"
  constructor() {
    makeObservable(this, {
      filter: observable,
      changeFilter: action,
      filterTodos: computed
    })
  }
  get filterTodos() {
    switch (this.filter) {
      case "all":
        return this.todos
      case "active":
        return this.todos.filter(todo => !todo.completed)
      case "completed":
        return this.todos.filter(todo => todo.completed)
      default:
        return this.todos
    }
  }
  changeFilter(filter) {
    this.filter = filter
  }
}
// TodoFooter.js
function TodoFooter() {
  const todoListStore = useTodoListStore()
  return (
    <footer className="footer">
      <ul className="filters">
        <li>
          <button
            onClick={() => todoListStore.changeFilter("all")}
            className={todoListStore.filter === "all" ? "selected" : ""}
          >
            All
          </button>
        </li>
        <li>
          <button
            onClick={() => todoListStore.changeFilter("active")}
            className={todoListStore.filter === "active" ? "selected" : ""}
          >
            Active
          </button>
        </li>
        <li>
          <button
            onClick={() => todoListStore.changeFilter("completed")}
            className={todoListStore.filter === "completed" ? "selected" : ""}
          >
            Completed
          </button>
        </li>
      </ul>
    </footer>
  )
}
// TodoListView.js
function TodoListView() {
  const todoListStore = useTodoListStore()
  return (
    <ul className="todo-list">
      {todoListStore.filterTodos.map(todo => <TodoView key={todo.id} todo={todo} />)}
    </ul>
  )
}

加载远端任务

  1. 下载并启动 json-server

npm install -g json-server

json-server ./src/todo.json --port 3005

yarn add axios

{
  "todos": [
    {
      "id": 1,
      "title": "React",
      "finished": false
    },
    {
      "id": 2,
      "title": "Angular",
      "finished": false
    },
    {
      "id": 3,
      "title": "Vue",
      "finished": false
    }
  ]
}


  1. 创建 loadTodos 方法用于加载初始任务
// TodoListStore.js
import axios from "axios"
import { runInAction } from "mobx"

class TodoListStore {
  constructor(todos) {
    this.loadTodos()
  }
  async loadTodos() {
    let todos = await axios.get("http://localhost:3005/todos").then(response => response.data)
    runInAction(() => todos.forEach(todo => this.todos.push(todo)))
  }
}
  1. 解决远端加载待办事项无法切换状态的问题

原因是远端加载待办事项的任务对象的原型对象上没有 toggle 方法。

import TodoStore from "./TodoStore"

class TodoListStore {
  async loadTodos() {
    runInAction(() => todos.forEach(todo => this.todos.push(new TodoStore(todo.title))))
  }
}

创建RootStore

通过创建 RootStore 可以将 CounterStore 和 TodoListStore 进行合并,实现在任何组件中都可以访问任何状态,方便全局状态共享。

// RootStore.js
import CounterStore from "./Counter/CounterStore"
import TodoListStore from "./Todos/TodoListStore"
import { createContext, useContext } from "react"

class RootStore {
  constructor() {
    this.counterStore = new CounterStore()
    this.todoListStore = new TodoListStore()
  }
}

const RootStoreContext = createContext()

const RootStoreProvider = ({ store, children }) => {
  return (
    <RootStoreContext.Provider value={store}>
      {children}
    </RootStoreContext.Provider>
  )
}

const useRootStore = () => {
  return useContext(RootStoreContext)
}

export { RootStore, RootStoreProvider, useRootStore }
// App.js
import Counter from "./components/Counter/Counter"
import TodoListView from "./components/Todos/TodoListView"
import { RootStore, RootStoreProvider } from "./stores/RootStore"

const rootStore = new RootStore()

function App() {
  return (
    <RootStoreProvider store={rootStore}>
      <TodoListView />
      <Counter />
    </RootStoreProvider>
  )
}

export default App
// 消费 RootStore 的其他组件
import { useRootStore } from "../../stores/RootStore"

function TodoHeader() {
  const { todoListStore } = useRootStore()
}

function Counter() {
  const { counterStore } = useRootStore()
}

数据检测

监控数据变化执行副作用,接收一个函数作为参数,参数函数用来执行副作用,当参数函数内部使用的 observable state, computed 发生变化时函数会运行,初始运行 autorun 方法时参数函数也会运行一次。

import { autorun } from "mobx"
import { useEffect } from "react"

function Counter() {
  const { counterStore } = useRootStore()
  useEffect(() => {
    // 确保 autorun 方法只被初始化一次
    autorun(() => {
      console.log(counterStore.count)
    })
  }, [])
}

对于基本数据类型,属于值传递,mobx 只能跟踪到原始属性,跟踪不到复制后的值。

useEffect(() => {
    let count = counterStore.count
    autorun(() => {
      // 错误写法, mobx 跟踪不到变量 count
      console.log(count)
    })
  }, [])

对于引用数据类型,只要引用地址不发生变化,mobx 就可以进行跟踪

// CounterStore.js
class CounterStore {
  person = { name: "张三" }
}

// Counter 组件
function Counter() {
  const { counterStore } = useRootStore()
  useEffect(() => {
    const person = counterStore.person
    autorun(() => {
      console.log(person.name)
    })
  }, [])
  return (
    <div>
      <p>{counterStore.person.name}</p>
      <button onClick={() => runInAction(() => (counterStore.person.name = "李四"))}>李四</button>
      <button onClick={() => runInAction(() => (counterStore.person = { name: "王五" }))}>王五</button>
    </div>
  )
}

监控状态变化执行副作用,接收两个函数作为参数,第一个函数返回要监控的状态,第二个函数用来执行副作用,只有当第一个函数返回的状态发生变化时,第二个函数才会执行。reaction 方法提供了更加细颗粒度的状态控制。

和 autorun 不同,reaction 初始时不会执行副作用。

import { reaction } from "mobx"

function Counter() {
	useEffect(() => {
  	reaction(
      () => counterStore.count,
      (current, previous) => {
        console.log(current)
        console.log(previous)
      }
    )
  }, []) 
}