最近在学习使用React & Mobx,学习过程中把一些经常使用到的点收集起来,汇总一下,方便后续查阅。
Mobx
MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程(transparently applying functional reactive programming – TFRP)使得状态管理变得简单和可扩展。MobX背后的哲学很简单:
- 任何源自应用状态的东西都应该自动地获得
- 其中包括UI、数据序列化、服务器通讯,等等。
浏览器支持情况
- Mobx >= 5 版本运行在任何支持 ES6 Proxy的浏览器
- Mobx 4 可以运行在任何支持ES5的浏览器上,而且也将持续地维护
Mobx 5和Mobx 4的API是相同的,并且语义上也能达到相同的效果,只是Mobx4存在一些局限性
React & Mobx 的关系
React和Mobx是相辅相成、相互合作的
React提供机制,把应用状态转换为可渲染组件树,并对其进行渲染。而Mobx提供机制来存储和更新应用状态,供React使用。
Mobx工作流程
- action, 事件调用,Action是唯一可修改state的函数,并且可能会有其它的副作用
- state,是可观察和最低限度定义的,不应该包含冗余或推导数据。可以是图形,包含类、数组、引用等等。
- computed values,是可以使用pure function(纯函数)从state中推导出来的值,Mobx会自动更新它,并在它不再使用时将其优化掉。
- Reactions,很像Computed values,会对state的变化做出反应,但它们不产生一个值,而是会产生一些副作用,如更新UI.
Mobx基本使用
本文使用的是Mobx 4.4版本,在项目中使用React+Mobx,主要介绍如下内容:
- 开发环境搭建
- Mobx常用API介绍
- 与React搭配使用时的常见问题
开发环境搭建
通过create-react-app创建一个typescript应用,具体的细节,这里不再述了,大家可以按照create-react-app的指引来操作.
大体步骤如下:
> npm install -g create-react-app
## 创建一个typescript应用
> create-react-app my-app --template typescript
> cd my-app
> npm install mobx@4.4.0 react-mobx
## 启动本地开发服务器, 默认会启用3000端口,通过http://localhost:3000来访问。
> npm run start
复制代码
Mobx常用API介绍
1. Observable 设置可观察数据
Observable是一个让数据的变化可以被观察的方法,底层是通过将该属性转化成getter/setter
来实现的。其值可以是JavaScript原始数据类型、引用类型、普通对象、类实例、数组和映射。
observable使用(装饰器模式)
import React from 'react'
import {observable, ObservableMap} from 'mobx'
export default class Demo extends React.Component{
// 基本数据类型
@observable num:number = 99
@observable str:string = 'hello james'
@observable flag:boolean = true
// 数组 对象
@observable list:Array<number> = [1,2,3]
@observable obj:object = {hello: 'james'}
// 映射(Map)
map:ObservableMap = observable.map({hi: 'map'})
componentDidMount(){
// 获取值
console.log(this.num, this.str, this.flag)
console.log(this.list, this.obj)
console.log(this.map.get('hi')
// 修改值
this.num = 100
this.str = 'hello world'
this.flag = false
this.list.push(4)
this.obj.hello = 'world'
this.map.set('hi', 'james')
}
}
复制代码
注意
1.在使用数组时,应避免下标越界去访问数组中的值,这不会被Mobx所监视,实际开发中,应注意数组长度的判断。
例如:
@observable list:Array<number> = [1,2,3]
// some code...
this.list[9] // undefined
复制代码
2.在使用对象时,如果需要动态添加新的属性,需要使用到extendObservable
, 否则新增的属性不会被Mobx所监视。
import {observable, extendObservable} from 'mobx'
// some code ...
@observable obj:object = {hello: 'james'}
// some code ...
extendObservable(this.obj, {
name: 'demo'
})
复制代码
2. computed 响应可观察数据的变化
computed values,计算值是可以根据现有的状态或其它计算值进行组合计算的值,可以使实际可修改的状态尽可能小。computed values是高度优化过的,应尽可能的多使用。
import React from 'react'
import {observable, computed} from 'mobx'
import { observer } from "mobx-react";
@observer
export default class Price extends React.Component{
@observable value = 2
@observable amount = 3
@computed get total(){
return this.value * this.amount
}
render(){
return <div>Total: {this.total}</div>
}
}
复制代码
computed的setter
computed的setter不能用来改变computed values,而是用来修改它里面的成员的值,从而使用得computed values发生变化。
import React from 'react'
import {observable, computed} from 'mobx'
import { observer } from "mobx-react";
@observer
export default class Price extends React.Component{
@observable value = 2
@observable amount = 3
@computed get total(){
return this.value * this.amount
}
// setter一定要定义在getter后,一些typescript版本会认为声明了两个名称相同的属性而报错
set total(val){
this.value = val
}
componentDidMount(){
console.log(this.total) // 6
this.total = 5
console.log(this.total) // 15
}
render(){
return <div>Total: {this.total}</div>
}
}
复制代码
注意
- 如果任何影响computed values的值发生变化了,computed values将根据状态自动进行变化。 如果值未发生变化,它也不会变化
- computed values在计算期间抛出异常,异常会被捕获,并在读取值的时候抛出异常。抛出的异常不会中断跟踪,所有计算值可以从异常中恢复。
import React from 'react'
import {observable, computed} from 'mobx'
import { observer } from "mobx-react";
@observer
export default class Price extends React.Component{
@observable value = 2
@observable amount = 3
@computed get total(){
if(this.value === 0 ){
throw new Error('value is 0')
}
return this.value * this.amount
}
set total(val){
this.value = val
}
componentDidMount(){
console.log(this.total) // 6
this.total = 0
console.log(this.total) // 报错, value is 0
this.total = 5
console.log(this.total) // 10
}
render(){
return <div>Total: {this.total}</div>
}
}
复制代码
3. autorun
autorun, 自动运行?是的,自动运行。
当观测到的数据发生变化的时候,如果变化的值处在autorun中,那么autorun就会自动执行。
import React from 'react'
import {observable, autorun} import 'mobx'
export default class Demo extends React.Component{
@observable hello = 'world'
@observable flag = false
componentDidMount(){
this.handler = autorun(() => {
console.log('current flag value is: ', this.flag)
if(flag){
console.log('ouput: => ", this.hello)
}
})
}
componentWillUnmount(){
// 为避免细微的内存问题,需要调用清理函数
this.handler()
}
render(){
return <div>
<a onClick={() => this.flag = !this.flag }>click me to</a>
</div>
}
}
复制代码
运行后,可以看到输出了“current flag value is false”, 当我们点击a
标签时,输出了:
current flag value is false
output: => world
复制代码
即,修改autorun中任意一个可观察的数据,即可触发自动运行。
3. when
when(提供了执行逻辑的条件,算是一种改进后的autorun), 接收两个函数参数:
- 第一个函数:根据可观察数据返回一个布尔值。当该布尔值为true时,执行第二个函数,且只执行一次。
- 第二个函数:如果可观察数据返回的布尔值一开始就是true,那么立即同步执行第二个函数。
import {when} from 'mobx'
// some code ...
@observable store: object = {
name: "james",
nick: "zhang",
count: 0,
};
// some code ...
componentDidMount(){
this.handler = when(
() => this.store.count > 10,
() => {
console.log("this.store.count value > 10", this.store.count);
}
);
}
componentWillUnmount(){
// 为避免细微的内存问题,需要调用清理函数
this.handler()
}
复制代码
4. reaction
reaction(通过分离可观察数据声明,以副作用的方式,对autorun做出了改进)
按需确定哪些是我们需要我们观察的数据,在这些数据发生变化时,主动触发副作用函数,接收两个函数参数:
- 第一个函数:引用可观察数据,并返回一个值,这个值会作为第二个函数的参数。
- 第二个函数:也叫副作用函数。
reaction(
() => [this.store.name, this.store.count],
(arr) => console.log("reaction => ", arr)
);
复制代码
reaction第一次渲染的时候,会先执行一次第一个函数,这使得Mobx知晓哪些可观察数据被引用。随后,在这些数据被修改时,就会执行第二个函数
5. action 修改可观察数据
autorun或reaction,在修改可观察数据时,都会触发并运行一次副作用函数,但是,通常情况下,这样的高频触发可能是没有必要的。如:用户多次点击了一个按钮,但实际可能只需触发一次回调即可。
此类场景,就比较适合使用action
action,是修改任何状态的行为,使用action的好处是能将多次修改可观察状态合并为一次,从而减少触发 autorun 或 reaction的次数。
import React from 'react'
import { observable, computed, reaction, action } from 'mobx'
export default class Demo extends React.Component{
@observable name = 'james'
@observable age = 30
@action
change(){
this.name = 'hello react'
this.age = 8
}
componentDidMount(){
reaction(() => [this.name, this.age], arr => {
console.log(arr)
})
}
render(){
return <div>
<button type="button" onClick={() => this.change()}>Change</button>
</div>
}
}
复制代码
当我们点击按钮“Change”时,会触发回调函数连续修改2个变量的值,此时会发现,控制台只输出了一次,即reaction只被执行了一次。
action.bound
action.bound可以用来自动地将动作绑定到目标对象,与action不同的是,action.bound不需要一个name参数,名称将始终基于动作绑定属性。
class Ticker {
@observable tick = 0
@action.bound
increment() {
this.tick++ // 'this' 永远都是正确的
}
}
const ticker = new Ticker()
setInterval(ticker.increment, 1000)
复制代码
action.bound 不要和箭头函数一起使用。箭头函数已经是绑定过的并且不能重新绑定
runInAction
runInAction是一个简单的工具函数,它接收代码块并在(异步的)动作中执行。 这对于即时创建各执行动作非常有用。例如, 在异步过程中,runInAction(f)是action(f)()的语法糖。
import React from 'react'
import { observable, computed, reaction, action, runInAction } from 'mobx'
export default class Demo extends React.Component{
@observable name = 'james'
@observable age = 30
@action.bound
change(){
this.name = 'hello react'
this.age = 8
}
componentDidMount(){
reaction(() => [this.name, this.age], arr => {
console.log(arr)
})
setTimeout(() => {
runInAction(() => {
this.name = 'hello python'
this.age = 31
})
}, 1000)
}
render(){
return <div>
<button type="button" onClick={this.change}>Change</button>
</div>
}
}
复制代码
使用小技巧
1. 动态添加Mobx 对象属性
在使用对象时,如果需要动态添加新的属性,需要使用到extendObservable
, 否则新增的属性不会被Mobx所监视。
import {observable, extendObservable} from 'mobx'
// some code ...
@observable obj:object = {hello: 'james'}
// some code ...
extendObservable(this.obj, {
name: 'demo'
})
复制代码
2. 尽早绑定函数
此技巧适用于普通的 React 和特别是使用了 PureRenderMixin 的库,尽量避免在 render 方法中创建新的闭包。
// bad
render() {
return <MyWidget onClick={() => { alert('hi') }} />
}
复制代码
在这个示例中, MyWidget 里使用的 PureRenderMixin 中的 shouldComponent 的返回值永远是 false,因为每当父组件重新渲染时你传递的都是一个新函数。
// good
render() {
return <MyWidget onClick={this.handleClick} />
}
handleClick = () => {
alert('hi')
}
复制代码
3. 优化React组件渲染
@observer 组件会追踪它们使用的所有值,并且当它们中的任何一个改变时重新渲染。 所以你的组件越小,它们需要重新渲染产生的变化则越小;这意味着用户界面的更多部分具备彼此独立渲染的可能性。
在专用组件中渲染列表
这点在渲染大型数据集合时尤为重要。
React 在渲染大型数据集合时表现非常糟糕,因为observer必须评估每个集合变化的集合所产生的组件。
因此,建议使用专门的组件来映射集合并渲染这个组件,且不再渲染其他组件:
// bad
@observer class MyComponent extends Component {
render() {
const {todos, user} = this.props;
return (<div>
{user.name}
<ul>
{todos.map(todo => <TodoView todo={todo} key={todo.id} />)}
</ul>
</div>)
}
}
复制代码
在这个示例中,当user.name
值发生改变时,React会不协调所有的ToDoView组件,尽管ToDoView组件不会重新渲染,但是协助的过程本身也是非常昂贵的。
// good
@observer class MyComponent extends Component {
render() {
const {todos, user} = this.props;
return (<div>
{user.name}
<TodosView todos={todos} />
</div>)
}
}
@observer class TodosView extends Component {
render() {
const {todos} = this.props;
return <ul>
{todos.map(todo => <TodoView todo={todo} key={todo.id} />)}
</ul>)
}
}
复制代码
4. 永远要清理reaction
所有形式的 autorun、 observe 和 intercept, 只有所有它们观察的对象都垃圾回收了,它们才会被垃圾回收。 所以当不再需要使用它们的时候,推荐使用清理函数(这些方法返回的函数)来停止它们继续运行。 对于 observe 和 intercept 来说,当目标是 this 时通常不是必须要清理它们。 对于像 autorun 这样的 reaction 要棘手得多,因为它们可能观察到许多不同的 observable,并且只要其中一个仍在作用域内,reaction 将保持在作用域内,这意味着其使用的所有其他 observable 也保持活跃以支持将来的重新计算。 所以当你不再需要 reaction 的时候,千万要清理掉它们!
import React from 'react'
import {observable, autorun} import 'mobx'
export default class Demo extends React.Component{
@observable hello = 'world'
@observable flag = false
componentDidMount(){
this.handler = autorun(() => {
console.log('current flag value is: ', this.flag)
if(flag){
console.log('ouput: => ", this.hello)
}
})
}
componentWillUnmount(){
// 为避免细微的内存问题,需要调用清理函数
this.handler()
}
render(){
return <div>
<a onClick={() => this.flag = !this.flag }>click me to</a>
</div>
}
}
复制代码