最近参与的项目中使用 MobX 进行状态管理,故看了一下官方文档,学习如何使用,将一些需要注意的点做些记录。
注意点
Observale state
- 如果项目中没有类组件,可以使用
mobx-react-lite替代mobx-react make(Auto)Observable仅支持已经定义的属性。请确保你的 编译器选项是正确的,或者,作为权宜之计,确保在你使用make(Auto)Observable之前已经为所有属性赋了值。如果没有正确的配置,已经声明而未初始化的字段(例如:class X { y; })将无法被正确侦测到。makeObservable只能注解由其本身所在的类定义声明出来的属性。如果一个子类或超类引入了可观察字段,那么该子类或超类就必须自己为那些属性调用makeObservable。- 只有定义在原型上的
action,computed,flow,action.bound可以在子类中被 overriden。 - 不支持 在调用
make(Auto)Observable之后 修改原型。 - 不支持 EcmaScript 中的私有字段(
#field)。使用 TypeScript 时,推荐改用private修饰符。
Actions
- 提示: 使用
makeAutoObservable(o, {}, { autoBind: true })自动绑定所有的 actions 和 flows runInAction使用这个工具函数来创建一个会被立即调用的临时action。await之后的任何操作都不与其同在一个 tick 中,因此它们需要使用 action 包装。 在这里,我们可以利用runInAction
使用 flow 代替 async / await
flow 包装器是一个可选的 async / await 替代方案,它让 MobX action 使用起来更加容易。
flow 将一个 generator 函数 作为唯一输入。 在 generator 内部,你可以使用 yield 串联 Promise(使用 yield somePromise 代替 await somePromise)。 flow 机制将会确保 generator 在 Promise resolve 之后继续运行或者抛出错误。
所以 flow 是 async / await 的一个替代方案,不需要再用 action 进行包装。它可以按照下面的方式使用:
- 使用
flow包装你的异步函数。 - 使用
function *代替async。 - 使用
yield代替await。
import { runInAction, makeAutoObservable } from "mobx"
class Store {
githubProjects = []
state = "pending" // "pending", "done" or "error"
constructor() {
makeAutoObservable(this)
}
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
runInAction(() => {
this.githubProjects = filteredProjects
this.state = "done"
})
} catch (e) {
runInAction(() => {
this.state = "error"
})
}
}
}
import { flow, makeAutoObservable, flowResult } from "mobx"
class Store {
githubProjects = []
state = "pending"
constructor() {
makeAutoObservable(this, {
fetchProjects: flow
})
}
// 注意星号, 这是一个 generator 函数!
*fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
// Yield 代替 await.
const projects = yield fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
this.state = "done"
this.githubProjects = filteredProjects
} catch (error) {
this.state = "error"
}
}
}
const store = new Store()
// 这里也可以不使用 flowResult,而直接使用 store.fetchProjects(),只不过需要在手动绑定 this,详细可以看官方文档
const projects = await flowResult(store.fetchProjects())
enforceActions 配置的目的是让你不会忘记使用 action 包裹事件处理函数。
Computeds
- 计算值可以有 setters
- computed.struct 如果两次的值相同,则不会通知观察者
- 可以使用 options 中的 equals 函数,来重写两次判断的比较
Reactions
reaction(
() => giraffe.isHungry,
isHungry => {
if (isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
console.log("Energy level:", giraffe.energyLevel)
}
)
when 会观察并运行给定的 predicate 函数,直到其返回 true。 一旦 predicate 返回了 true,给定的 effect 函数就会执行并且自动执行器函数将会被清理掉。
如果你没有传入 effect 函数,when 函数返回一个 Promise 类型的 disposer,并允许你手动取消。
注意:这个 when 只会触发一次,后面无论如何都不在触发
import { when, makeAutoObservable } from "mobx"
class MyResource {
constructor() {
makeAutoObservable(this, { dispose: false })
when(
// Once...
() => !this.isVisible,
// ... then.
() => this.dispose()
)
}
get isVisible() {
// 表示此项目是否可见.
}
dispose() {
// 清理一些资源.
}
}
Always dispose of reactions
比如列表里面的 item 是 reaction的,当 item 被 remove 掉的时候就要 dispose。
原则
- 只有在引起副作用的一方与副作用之间没有直接关系的情况下才使用 reaction,比如你
- reactions 不应该更新其他可观察对象
- reactions 应该是独立的
MobX 与 React
import { observer, useLocalObservable } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
你可能并不需要全局的可观察状态
通常来讲,我们推荐在编写全局公用组件的时候不要立刻使用Mobx的可观察能力, 因为从技术角度来讲他可能会使你无法使用一些React 的 Suspense 的方法特性。
使用Mobx的可观察能力作为 React components 的一种状态补充,比如出现以下情况: 1) 层级很深, 2) 拥有计算属性 3) 需要共享状态给其它 observer components。
始终在observer 组件中使用可观察能力
- 尽可能晚的从对象中获取值
- 不要将可观察对象传递到 不是
observer的组件中,应该使用toJS(xx)包一下 render props可能会需要<Observer>
useEffect 与可观察对象
import { observer, useLocalObservable, useAsObservableSource } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
// 在Effect方法之上触发可观察对象变化。
useEffect(
() =>
autorun(() => {
if (timer.secondsPassed > 60) alert("Still there. It's a minute already?!!")
}),
[]
)
// 作为demo用途在Effect里定义一个定时器。
useEffect(() => {
const handle = setInterval(timer.increaseTimer, 1000)
return () => {
clearInterval(handle)
}
}, [])
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
React 优化
- 使用大量的小组件
- 专用组件去渲染列表
- 不要使用数组的索引作为 key
- 晚一点使用间接引用值
提示与技巧
一个经常被问到的问题是:如何在不使用单例的情况下组合多个 stores,stores 之间如何相互通信?
class RootStore {
constructor() {
this.userStore = new UserStore(this)
this.todoStore = new TodoStore(this)
}
}
class UserStore {
constructor(rootStore) {
this.rootStore = rootStore
}
getTodos(user) {
// 通过 root store 来访问 todoStore
return this.rootStore.todoStore.todos.filter(todo => todo.author === user)
}
}
class TodoStore {
todos = []
rootStore
constructor(rootStore) {
makeAutoObservable(this, { rootStore: false })
this.rootStore = rootStore
}
}
使用 React 时,root store 一般通过 React context 插入到组件树中。
自定义 observables
使用 creatAtom
创建惰性 observables
onBecomeObserved和onBecomeUnobserved方法可以给现有的可观察对象附加惰性行为或副作用。它们是MobX可观察系统的钩子并且 当可观察对象开始和停止被观察时,它们会得到通知。它们都返回一个用来取消listener的disposer函数。
export class City {
location
temperature
interval
constructor(location) {
makeAutoObservable(this, {
resume: false,
suspend: false
})
this.location = location
// 只有在实际使用温度时才开始获取数据!
onBecomeObserved(this, "temperature", this.resume)
onBecomeUnobserved(this, "temperature", this.suspend)
}
resume = () => {
log(`Resuming ${this.location}`)
this.interval = setInterval(() => this.fetchTemperature(), 5000)
}
suspend = () => {
log(`Suspending ${this.location}`)
this.temperature = undefined
clearInterval(this.interval)
}
fetchTemperature = flow(function* () {
// 获取数据的逻辑...
})
}
待探索
如果使用这个框架,不知道能不能将 react 和 vue 两端的逻辑进行抽离,如小程序和 RN 上面逻辑一致,但是得写两份。(茴字的二种写法?)
不好的感受
- 有
hooks后,还要写class,就和吃苍蝇一样难受 - 有可能会导致如
suspense的特性