注意:这不是教学,仅仅是学习笔记
Mobx 原则
- Mobx 是单向的数据流,也就是 Action 改变 state,state 的更新会改变所有影响的视图。
- 当 state 更新时,所有衍生都会进行原子级的自动更新。因此永远不肯能观察到中间值。
- 所有衍生默认都是同步更新。这意味这,动作可以在改变状态之后直接可以安全的检查计算值。
- 计算值是延迟更新的。任何不在使用中的计算值将不会再进行计算,如果视图不再使用,则计算值会被回收。
- 所有计算值都应该是纯净的,它们不应该用来更新 状态
1. 将数据转换为 Observable 数据。
- Map,Array,Object 转化为 Observable 数据。
- Map 的每个属性都将会转换为可观察数据,当频繁的对 对象属性增删时,使用 Map 会比较好
- Object 的每个属性都会转换成可观察数据,如果属性值是对象,也会递归转换。
- 对于基本数据类型,需要使用 observable.box 将其转换为观察数据。
import { observable } from 'mobx'
const ob_map = observable(new Map({name: 'map data'}))
const ob_array = observable([1,2,3])
ob_array[1] = 3
const ob_object = observsble({
name: 'xiaobai',
age: 18
})
ob_object.name = 'xiaohei'
const ob_num = observable.box(25)
ob_num = 20
- 将 class 进行 Observable 转换
- 能使用装饰器环境时,使用 @observable 对属性进行包装。
- 在不能使用时,需要 decorate 对类进行包装。
import { observable, decorate, action } from 'mobx'
// 使用装饰器
class Person {
@observable name = 'xiaobao'
@observable age = 18
@action.bound
changeAge(age) {
this.age = age
}
}
// 不使用装饰器时
class Person {
name;
age;
constructor(){
this.name = 'xiaobao'
this.age = 18
}
changeAge(age) {
this.age = age
}
}
decorate(Person, {
name: observable,
age: observable,
changeAge: action.bound
})
3. 关于 Observable.box
observable.box(value) 可以将任意值储存在 box 中。使用 .get() 可以获取值,使用 .set(value) 可以设置值。
import {observable} from 'mobx'
const ob_num = observable.box(20)
ob_num.get() // 20 获取值
ob_num.set(25) // 替换当前储存的值,并通知所有使用的观察者。
ob_num.interept(interceptor) // 可以用来在任何变化时将其拦截
ob_num.observe() // 注册一个观察者,在每次储存值被替换时触发。返回一个取消注册的函数。
observable.box(value, {name: 'value'}) name 选项用来给 value 一个更好的调试名称。
observable.box(value, {deep: false}) 盒子里的任何值都不会转换为可观察对象。
4. Observable 装饰器合集
observable 定义了一系列装饰器来定义行为
名称 | 描述 |
---|---|
observable | observable.deep 的别名 |
observable.deep | 任何 observable 都是用的默认的调节器,他将任何(尚未转换为 observable)数组,映射或者纯对象克隆并将其转换为 observble 对象。 |
observable.ref | 禁用 observable 的自动转换,值创建一个 observable 的引用 |
observable.shallow | 只能与数组配合使用。将任何集合转换为 observable 对象,但该集合的值不会进行转换 |
observable.struct | 就像 ref,但会忽略结构上等于当前值的新值(值改变时才会刷新应用) |
computed | 创建一个衍生值,根据某个观察值衍生出来的数据 |
computed(option) | 与 computed 相同,可以配置选项 |
computed.struct | 与 computed 相同,但是只有当视图产生的值与之前的值结构不相同是,才会通知观察者 |
action | 创建一个动作 |
action(name) | 创建一个动作,但是重载了名称 |
action.bound | 创建一个动作,并将 this 绑定到实例 |
5. (@)computed
- 计算值是根据现有状态值或者其他计算值衍生出来的值。
- 计算属性是不可枚举的,而且不能在原形链中被覆盖。
// @computed
// 如果启用了装饰器,可以在任意属性的 getter 上使用 @computed
import { observable, computed } from 'mobx'
class Person {
@observable name = 'xiaobai'
@observable age = 18
@computed get person(){
return `${this.name}-${this.age}`
}
}
// 如果未开启 装饰器,可以使用 decorate
import {decorate, observable, computed} from 'mobx'
class Person1{
name
age
constructor(){
this.name = 'xiaobai'
this.age = 18
}
get person(){
return `${this.name}-${this.age}`
}
}
decorate(Person1, {
name: observable,
age: observable,
person: computed
})
6. observable.object 和 extendObservable
这两个 API 会自动将 getter 属性转换为计算属性。
import {observable, extendObservable} from 'mobx'
const ob_ob1 = observable.object({
name: 'box',
age: 18,
get person(){
return `${this.name}-${this.age}`
}
})
// extendObservable 版本
extendObservable({
name: 'box',
age: '18',
get person(){
return `${this.name}-${this.age}`
}
})
// extendObservable 会将所有属性转换为可观察值。新添加的属性也会自动转换为可观察值。
关于 setter
还可以指定计算值的 setter ,但是不能用来改变计算值,可以用来逆向衍生。
import {computed, observable} from 'mobx'
class Person{
@observable name = 'xiaobai'
@observable age = 18
computed get person(){
return `${this.name}-${this.age}`
}
set person(value) {
this.age = value + 2
}
}
// 注意: setter 需要在 getter 后定义
computed(expression)函数
computed 可以直接作为函数被调用,就像 observable.box(value) 一样调用,并获得一个 observable。
import {observable, computed} from "mobx";
var name = observable("John");
var upperCaseName = computed(() =>
name.get().toUpperCase()
);
upperCaseName .get() // 20 获取值
upperCaseName .set(25) // 替换当前储存的值,并通知所有使用的观察者。
upperCaseName .interept(interceptor) // 可以用来在任何变化时将其拦截
var disposer = upperCaseName.observe(change => console.log(change.newValue));
// 注册一个观察者,在每次储存值被替换时触发。返回一个取消注册的函数
name.set("Dave");
// 输出: 'DAVE'
7. Autorun
当你想创建一个响应式函数,但该函数本身永远不会有观察者时,可以使用 mobx.autorun。
autorun 会在创建时就运行一次。
import {observable, autorun, computed} from 'mobx'
const arr = observable([1,2,3])
const sum = computed(() => arr.reduce(sum, item) => sum + item)
const disposer = autorun( () => { console.log(sum.get()) } )
// 输出 6
arr.push(4)
// 输出 10
disposer()
arr.push(5)
// 不会在输出任何值
选项
Autorun 可以接受第二个参数,有如下参数可选:
属性 | 描述 |
---|---|
delay | 可用于效果函数的去抖时间(单位是毫秒)。默认为 0,不去抖 |
name | 字符串,方便调试 |
onError | 处理 Autorun 的错误,而不是传播下去 |
scheduler | 设置自定义调度器以决定如何调度 autorun 函数的重新运行 |
8. when
when(predicate: () => boolean, effect?: () => void, options?)
当 predicate 返回值为 true 时,才会执行 effect 函数。返回一个取消函数,方便手动取消。注意,只要返回 true,就会运行 effect 函数,然后就会被取消。
import {computed, when} from 'mobx'
class A{
constructor(){
when(
() => this.visible,
() => {this.dispose}
)
}
@computed get visible(){
return true
}
dispoes(){
// 做一些操作
}
}
when-promise
如果没提供 effect 函数,则会返回一个 Promise,可以与 async/await 结合使用
import {when} from 'mobx'
async function(){
await when(() => true)
}
9. Reaction
// 用法
reaction(() => data, (data, reaction) => {sideEffect}, options)
reaction 在创建时不会运行,只有当返回的 data 有变化时,才会运行第二个参数,reaction 会返回一个清理函数。
下面给出一则完整的演示示例:
import {reaction, observable, autorun} from 'mobx'
const ob = observable([{
title: 'xiaobai',
age: 18
},
{
title: 'xiaohei',
age: 19
}
])
// reaction1 错误示例:对length做出反应,而不是对 title
const reaction1 = reaction(
() => ob.length,
(data, reaction) => {
console.log('reaction1',data.map(item) => item.title).join(',')
}
)
// reaction 的正确用法,对 length 和 title 的变化做出反应
const reaction2 = reaction(
() => ob.map(item => item.title).join(','),
(data, reaction) => {console.log('reaction2',data)}
)
//autorun 对任何的数据改变做出反应
const autorun = autorun( () => {
console.log('autorun', ob.map(item => item.title).join(','))
} )
todos.push({ title: "explain reactions", done: false });
// 输出:
// reaction 1: xiaobai, xiaohei, explain reactions
// reaction 2: xiaobai, xiaohei, explain reactions
// autorun 1: xiaobai, xiaohei, explain reactions
todos[0].title = "Make tea"
// 输出:
// reaction 2: xiaobai, xiaohei, explain reactions
// autorun 1: xiaobai, xiaohei, explain reactions
下面示例中,reaction3 会对 counter 的 count 做出反应。当调用 reaction 是第二个参数会作为清理参数使用。
import {reaction, observable} from 'mobx'
const num = observable({count: 0})
// 只调用一次 reaction ,对 observable 的 count 做出反应。
const reaction1 = reaction(
() => num.count,
(data, reaction) => {
console.log(data)
reaction.dispose()
}
)
counter.count = 1;
// 输出:
// reaction 3: invoked. counter.count = 1
counter.count = 2;
// 输出:
// (There are no logging, because of reaction disposed.
// But, counter continue reaction)
console.log(counter.count);
// 输出:
// 2
粗略地讲,reaction 是 computed(expression).observe(action(sideEffect)) 或 autorun(() => action(sideEffect)(expression) 的语法糖。
10. action
用法:
action(fn)
action(name, fn)
@action classMethod(){}
@action(name) classMethod(){}
@action boundClassMethod = () => {}
@action(name) boundClassMethod = () => {}
@action.bound classMethod
何时使用动作
永远只在修改状态时使用动作。
绑定动作
装饰器/函数遵循 javascript 中标准的绑定规则。 但是,action.bound 可以用来自动地将动作绑定到目标对象。 注意,与 action 不同的是,(@)action.bound 不需要一个name参数,名称将始终基于动作绑定的属性。
注意: action.bound 不要和箭头函数一起使用;箭头函数已经是绑定过的并且不能重新绑定。
示例:
class Ticker {
@observable tick = 0
@action.bound
increment() {
this.tick++ // 'this' 永远都是正确的
}
}
const ticker = new Ticker()
setInterval(ticker.increment, 1000)
runInAction(name?, thunk)
runInAction 是个简单的工具函数,它接收代码块并在(异步的)动作中执行。这对于即时创建和执行动作非常有用,例如在异步过程中。runInAction(f) 是 action(f)() 的语法糖。
11. 编写异步 Actions
action 包装/装饰器只会对当前运行的函数作出反应,而不会对当前运行函数所调用的函数(不包含在当前函数之内)作出反应! 这意味着如果 action 中存在 setTimeout、promise 的 then 或 async 语句,并且在回调函数中某些状态改变了,那么这些回调函数也应该包装在 action 中。创建异步 action 有几种方式。不能说某种方式一定比其他的好,本章只是列出编写异步代码的几种不同方式而已。 我们先从一个基础的示例开始:
Promise
mobx.configure({ enforceActions: true }) // 不允许在动作之外进行状态修改
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
projects => {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
},
error => {
this.state = "error"
}
)
}
}
上面的示例会抛出异常,因为传给 fetchGithubProjectsSomehow promise 的回调函数不是 fetchProjects 动作的一部分,因为动作只会应用于当前栈。
首选的简单修复是将回调函数变成动作。(注意使用 action.bound 绑定在这很重要,以获取正确的 this!):
mobx.configure({ enforceActions: true })
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
this.fetchProjectsSuccess,
this.fetchProjectsError
)
}
@action.bound
fetchProjectsSuccess(projects) {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}
@action.bound
fetchProjectsError(error) {
this.state = "error"
}
}
尽管这很整洁清楚,但异步流程复杂后可能会略显啰嗦。另外一种方案是你可以使用 action关键字来包装 promises 回调函数。推荐这么做,但不是强制的,还需要给它们命名:
mobx.configure({ enforceActions: true })
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
// 内联创建的动作
action("fetchSuccess", projects => {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}),
// 内联创建的动作
action("fetchError", error => {
this.state = "error"
})
)
}
}
runInAction 工具函数
内联动作的缺点是 TypeScript 无法对其进行类型推导,所以你应该为所有的回调函数定义类型。 你还可以只在动作中运行回调函数中状态修改的部分,而不是为整个回调创建一个动作。 这种模式的优势是它鼓励你不要到处写 action,而是在整个过程结束时尽可能多地对所有状态进行修改:
mobx.configure({ enforceActions: true })
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
projects => {
const filteredProjects = somePreprocessing(projects)
// 将‘“最终的”修改放入一个异步动作中
runInAction(() => {
this.githubProjects = filteredProjects
this.state = "done"
})
},
error => {
// 过程的另一个结局:...
runInAction(() => {
this.state = "error"
})
}
)
}
}
注意,runInAction 还可以给定第一个参数作为名称。runInAction(f) 实际上是 action(f)() 的语法糖。
async / await
基于 async / await 的函数当开始使用动作时起初似乎会令人感到困惑。 因为在词法上它们看起来是同步函数,它给人的印象是 @action 应用于整个函数。 但事实并非若此,因为 async / await 只是围绕基于 promise 过程的语法糖。 结果是 @action 仅应用于代码块,直到第一个 await 。 在每个 await 之后,一个新的异步函数将启动,所以在每个 await 之后,状态修改代码应该被包装成动作。 这正是 runInAction 再次派上用场的地方:
mobx.configure({ enforceActions: true })
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// await 之后,再次修改状态需要动作:
runInAction(() => {
this.state = "done"
this.githubProjects = filteredProjects
})
} catch (error) {
runInAction(() => {
this.state = "error"
})
}
}
}
flows
然而,更好的方式是使用 flow 的内置概念。它们使用生成器。一开始可能看起来很不适应,但它的工作原理与 async / await 是一样的。只是使用 function * 来代替 async,使用 yield 代替 await 。 使用 flow 的优点是它在语法上基本与 async / await 是相同的 (只是关键字不同),并且不需要手动用 @action 来包装异步代码,这样代码更简洁。
flow 只能作为函数使用,不能作为装饰器使用。 flow 可以很好的与 MobX 开发者工具集成,所以很容易追踪 async 函数的过程。
flow 的关键作用是 处理异步代码时确保代码被action包装 ,因为正常的 observable state 对异步操作无法通过 enforceActions 检查。
import { configure, flow } from 'mobx';
mobx.configure({ enforceActions: true })
class Store {
@observable githubProjects = []
@observable state = "pending"
fetchProjects = flow(function * () { // <- 注意*号,这是生成器函数!
this.githubProjects = []
this.state = "pending"
try {
const projects = yield fetchGithubProjectsSomehow() // 用 yield 代替 await
const filteredProjects = somePreprocessing(projects)
// 异步代码块会被自动包装成动作并修改状态
this.state = "done"
this.githubProjects = filteredProjects
} catch (error) {
this.state = "error"
}
})
// 注意调用模式
@action actionFlow(){
this.fetchProjects()
}
}
Flows 可以撤销,调用promise的cancel() 方法会停止异步状态取值, 会继续执行 finally 子句 。