mobx 学习笔记
mobX的身份证
1.mobX是什么
- Simple,scalable state management
翻译过来就是简单和可扩展的状态管理
一个应用的正常运行,少不了众多的数据变量来记录某个时刻的状态,从而影响应用下一个影响的状态,特别是对于高度封装的组件化模块而言,往往需要内部状态变量来维持组件自身的独立运行,当应用复杂度开始上升以后,数据变量的数量也就急剧增多,并且他们之间的变化也会相互影响,管理不断变化的状态十分困难。比如在一个经典的MVC模型中,如果一个model变化影响另一个model变化时,那么当view变化时就可能引起对应model以及另一个model的变化,依次的可能会导致另一个view变化,直至你搞不清到底发生了什么,由次造成的后果是状态在什么时候、由于什么原因、如何变化已然不受控制。为了解决这些问题业界开始引入了状态管理工具的概念,来单独负责管理业务的重要状态数据,mobX就是最新的状态管理工具。
2. mobx和redux的关系
mobx vs redux
说到状态管理我们就不得不提到redux,同样都是管理应用程序的状态,mobx与redux之间有什么区别呢?
- 开发难度低
redux的api 是纯函数式编程风格的,这对于一般的只具备面向对象开发风格的人来说特别是经验尚浅的入门开发者而言,即使是它的原理和思想是如此的简单,使用redux也没有那么容易。
而mobx使用了语义丰富的响应式编程风格,对面向对象匹配更简略的api语法,大大降低了学习成本,同时mobx的集成度也比redux稍高,避免了让开发者引入众多的零散的第三方库。
- 开发代码量少
redux应用action、reducer、store等众多概念,每增加一个状态都要同步更新这些位置,样板代码较多,而mobx只要在store中更新即可。在代码编写量上,大大少于redux。
- 渲染性能好
我们知道在react组件中合理编写 shouldComponentUpdate方法可以避免不必要的重渲染提升页面性能,但是如果数据层次太负责的话实现这个方法并非易事。mobx并不存在这个问题,因为它能精确描述哪些组件是需要重新渲染的哪些不需要重渲染,通过合理的组织组件层级和数据结构位置可以轻易的将数据重渲染限制在最小的范围内,从而影响页面性能。
综合来说就是mobx能提供比redux学习成本更低,性能优化更友好的解决方案。
学习mobx不仅仅是为了今后的开发中提供一种新的技术选择,同样也可以给自己打开一个新的思路,举一反三,在其他领域的开发实践中也能利用类似的原理优化和开发产品的技术和管理指标,提升个人的技术视野和核心竞争力。
特别要强调的是虽然mobx的诞生要比redux要晚不少,但是业界已经收到了越来越多的关注,到目前为止,github上已经积累了19651个star,npm的周下载量已经到27万多,越来越多的开发者开始关注如何在mobx和redux中做选择。
mobx的核心思想
状态变化引起的副作用应该被自动触发
在mobx中它的这句话包括了两方面的意思:
- 应用逻辑只需要修改状态数据即可,mobx会自动触发些缓存,渲染UI等这些业务经历的副作用,无需人工干预。
- 副作用依赖哪些状态数据是被自动收集的,比如某个副作用依赖A和B,那么如果状态C发生变化,这个副作用是不会被触发的,这是mobx最吸引人的特性之一,也是mobx能够轻易优化渲染性能的关键所在。
在控制流上,mobx也应用的单项数据流模式。
Action
Action --> State --> Reaction
基础语法知识
前置知识:
定义类的语法是能让mobx发挥最大价值的语法。
为什么需要定义类?
javascript 是一门面向对象的语言。但组织大型应用逻辑的时候,我们不可避免的需要用类来封装和复用代码逻辑。
在OOP语言中我们需要注意的两个特性是
-
继承
-
多态
新建一个项目来说明:
mkdir mobx-test
cd mobx-test
mkdir src
touch src/index.js
npm init -y
touch webpack.config.js
安装依赖
npm i webpack webpack-cli @babel/core babel-preset-env babel-loader -D
babel-loader:webpack 和 babel 的桥梁
// webpack.config.js
const path = require('path')
const config = {
mode:'development',
entry:path.resolve(__dirname,'src/index.js'),
output:{
path:path.resolve(__dirname,'dist'),
filename:'main.js'// 文件名设置
},
// 定义loader 指示webpack如何来编译代码
module: {
rules:[{
// 用test 来匹配所有的js文件
test: /.js$/,
// 忽略node_modules下的文件
exclude:/node_modules/,
// 定义loader
use:{
loader:'babel-loader',
options: {
presets:['env'],
}
}
}]
},
// 调试用
devtool: 'inline-source-map'
}
module.exports = config
// package.json
"scripts": {
"start": "webpack -W"
},
webpack 和 webpack -w 区别
webpack -w可以对项目打包并且实时监控, 当前配置文件下的文件如果发生更改时重新打包, 但如果webpack的配置文件即webpack.config.js更改时还是要通过webpack进行打包.(退出webpack -w 操作 ctrl+c)
执行 npm run start
就能看到一个dist的文件夹
touch index.html
在index.html中引用 dist/main.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
<script src='./dist/main.js'></script>
</html>
现在我们就可以安心编写逻辑了,webpack 会把最新的打包代码给index.html所用
实现一个继承:
function Animal(){}
function Dog(){}
Object.defineProperties(Animal.prototype,{
name:{
value(){
return 'Animal'
}
},
say:{
value(){
return `I'm ${this.name()}`
}
}
})
// dog.instanceof.Animal => true
// dog.__proto__.__proto__... === Animal.prototype
//
// dog.__proto__ === Dog.prototype
// Dog.prototype.__proto__ === Animal.prototype
Dog.prototype = Object.create(Animal.prototype,{
constructor:{
value:Dog,
enumerable:false
},
name:{
value(){
return 'Dog'
}
}
})
document.write(new Dog().say())
console.log(Dog.prototype.constructor)
踩坑:
https://www.cnblogs.com/Joe-and-Joan/p/10335881.html
npm i babel-plugin-transform-class-properties -D
// webpack.config.js
const path = require('path')
const config = {
mode:'development',
entry:path.resolve(__dirname,'src/index.js'),
output:{
path:path.resolve(__dirname,'dist'),
filename:'main.js'// 文件名设置
},
// 定义loader 指示webpack如何来编译代码
module: {
rules:[{
// 用test 来匹配所有的js文件
test: /.js$/,
// 忽略node_modules下的文件
exclude:/node_modules/,
// 定义loader
use:{
loader:'babel-loader',
options: {
presets: ["@babel/preset-env"],
plugins:['transform-class-properties']
}
}
}]
},
// 调试用
devtool: 'inline-source-map'
}
module.exports = config
2.2 基础语法知识-decorator
- Decorator 是在声明阶段实现
类与类成员
注解的一种语法。
API
可观察的数据(observable)
对可观察的数据作出反应
修改可观察的数据
掌握了这几个概念以及它所对应的API以后也就具备了使用mobx维护应用层状态的能力。
observable
是一种让数据的变化可以被观察的方法。
那些数据类型可被观察?理论上来讲所有的数据都可能被观察,但我们常用的无外呼就是这么几种。
mobx对任意变量的处理方式有两种:
第一: 对于数组、纯对象 以及Es6当中的map,直接把observable当作函数来把变量转化为可观察的对象。之后对数组、对象、map中内部数据进行合理的操作,都将会被监视。
第二:除了以上类型,其他的类型都要用observable.box来把变量包装为可观察的对象,之后对该变量的直接赋值将会被监视。
接下来看代码:
import {observable} from 'mobx'
// observable.box
// array object map
//array
const arr = observable(['a','b','c'])
console.log(arr,Array.isArray(arr))
被包装以后就不是数组了。
// object
const obj = observable({a: '11', b: '22'})
console.log(obj.a, obj.b) // 11 22
obj.a = "leo";
console.log(obj.a, obj.b) // leo 22
// map
const map = observable.map({ key: "value"});
map.set("key", "new value");
console.log(map.has('key')) // true
map.delete("key");
console.log(map.has('key')) // false
@observable 使用
MobX 也提供使用装饰器 @observable 来将其转换成可观察的,可以使用在实例的字段和属性上。
import {observable} from "mobx";
class Leo {
@observable arr = [1];
@observable obj = {};
@observable map = new Map();
@observable str = 'leo';
@observable num = 100;
@observable bool = false;
}
let leo = new Leo()
console.log(leo.arr[0]) // 1
相比于前面使用 observable.box()方法对JS原始类型(Number/String/Boolean)进行定义,装饰器 @observable 则可以直接定义这些类型。
原因是装饰器 @observable 更进一步封装了 observable.box()。
2.响应可观察数据的变化
2.1 (@)computed
计算值
(computed values)是可以根据现有的状态或其它计算值进行组合计算的值。可以使实际可修改的状态尽可能的小。
此外计算值还是高度优化过的,所以尽可能的多使用它们。
可以简单理解为:它是相关状态变化时自动更新的值
,可以将多个可观察数据合并成一个可观察数据,并且只有在被使用时才会自动更新
。
知识点:使用方式
- 使用方式1:声明式创建
import {observable, computed} from "mobx";
class Money {
@observable price = 0;
@observable amount = 2;
constructor(price = 1) {
this.price = price;
}
@computed get total() {
return this.price * this.amount;
}
}
let m = new Money()
console.log(m.total) // 2
m.price = 10;
console.log(m.total) // 20
- 使用方式2:使用 decorate 引入
import {decorate, observable, computed} from "mobx";
class Money {
price = 0;
amount = 2;
constructor(price = 1) {
this.price = price;
}
get total() {
return this.price * this.amount;
}
}
decorate(Money, {
price: observable,
amount: observable,
total: computed
})
let m = new Money()
console.log(m.total) // 2
m.price = 10;
console.log(m.total) // 20
- 使用方式3:使用 observable.object 创建
observable.object 和 extendObservable 都会自动将 getter 属性推导成计算属性,所以下面这样就足够了:
import {observable} from "mobx";
const Money = observable.object({
price: 0,
amount: 1,
get total() {
return this.price * this.amount
}
})
console.log(Money.total) // 0
Money.price = 10;
console.log(Money.total) // 10
- 注意点
如果任何影响计算值的值发生变化了,计算值将根据状态自动进行变化。
如果前一个计算中使用的数据没有更改,计算属性将不会重新运行。 如果某个其它计算属性或 reaction 未使用该计算属性,也不会重新运行。 在这种情况下,它将被暂停。
知识点:computed 的 setter
computed 的 setter 不能用来改变计算属性的值,而是用来它里面的成员,来使得 computed 发生变化。
这里我们使用 computed 的第一种声明方式为例,其他几种方式实现起来类似:
import {observable, computed} from "mobx";
class Money {
@observable price = 0;
@observable amount = 2;
constructor(price = 1) {
this.price = price;
}
@computed get total() {
return this.price * this.amount;
}
set total(n){
this.price = n + 1
}
}
let m = new Money()
console.log(m.total) // 2
m.price = 10;
console.log(m.total) // 20
m.total = 6;
console.log(m.total) // 14
从上面实现方式可以看出,set total 方法中接收一个参数 n 作为 price 的新值,我们调用 m.total 后设置了新的 price,于是 m.total 的值也随之发生改变。
注意:
一定在 geeter 之后定义 setter,一些 typescript 版本会认为声明了两个名称相同的属性。
知识点:computed(expression) 函数
一般可以通过下面两种方法观察变化,并获取计算值:
-
方法1: 将 computed 作为函数调用,在返回的对象使用 .get() 来获取计算的当前值。
-
方法2: 使用 observe(callback) 来观察值的改变,其计算后的值在 .newValue 上。
import {observable, computed} from "mobx";
let leo = observable.box('hello');
let upperCaseName = computed(() => leo.get().toUpperCase())
let disposer = upperCaseName.observe(change => console.log(change.newValue))
leo.set('pingan')
- 知识点:错误处理
计算值在计算期间抛出异常,则此异常会被捕获,并在读取其值的时候抛出异常
。
抛出异常不会中断跟踪
,所有计算值可以从异常中恢复。
import {observable, computed} from "mobx";
let x = observable.box(10)
let y = observable.box(2)
let div = computed(() => {
if(y.get() === 0) throw new Error('y 为0了')
return x.get() / y.get()
})
div.get() // 5
y.set(0) // ok
div.get() // 报错,y 为0了
y.set(5)
div.get() // 恢复正常,返回 2
小结
用法:
-
computed(() => expression)
-
computed(() => expression, (newValue) => void)
-
computed(() => expression, options)
-
@computed({equals: compareFn}) get classProperty() { return expression; }
-
@computed get classProperty() { return expression; }
还有各种选项可以控制 computed 的行为。包括:
-
equals: (value, value) => boolean 用来重载默认检测规则的比较函数。 内置比较器有: comparer.identity, comparer.default, comparer.structural;
-
requiresReaction: boolean 在重新计算衍生属性之前,等待追踪的 observables 值发生变化;
-
get: () => value) 重载计算属性的 getter;
-
set: (value) => void 重载计算属性的 setter;
-
keepAlive: boolean 设置为 true 以自动保持计算值活动,而不是在没有观察者时暂停;
2.2 autorun
概念
autorun 直译就是自动运行的意思,那么我们要知道这两个问题:
- 自动运行什么?
即:自动运行传入 autorun 的参数函数。
import { observable, autorun } from 'mobx'
class Store {
@observable str = 'leo';
@observable num = 123;
}
let store = new Store()
autorun(() => {
console.log(`${store.str}--${store.num}`)
})
// leo--123
可以看出 autorun 自动被运行一次,并输出 leo--123 的值,显然这还不是自动运行。
- 怎么触发自动运行?
当修改 autorun 中任意一个可观察数据即可触发自动运行。
// 紧接上部分代码
store.str = 'pingan'
// leo--123
// pingan--123
现在可以看到控制台输出这两个日志,证明 autorun 已经被执行两次。
知识点:观察 computed 的数据
import { observable, autorun } from 'mobx'
class Store {
@observable str = 'leo';
@observable num = 123;
@computed get all(){
return `${store.str}--${store.num}`
}
}
let store = new Store()
autorun(() => {
console.log(store.all)
})
store.str = 'pingan'
// leo--123
// pingan--123
可以看出,这样将 computed 的值在 autorun 中进行观察,也是可以达到一样的效果,这也是我们实际开发中常用到的。
知识点
:computed 与 autorun 区别相同点
:
都是响应式调用的表达式;
不同点
:
-
@computed 用于响应式的产生一个可以被其他 observer 使用的值;
-
autorun 不产生新的值,而是达到一个效果(如:打印日志,发起网络请求等命令式的副作用);
-
@computed中,如果一个计算值不再被观察了,MobX 可以自动地将其垃圾回收,而 autorun 中的值必须要手动清理才行。
小结
-
autorun 默认会执行一次,以获取哪些可观察数据被引用。
-
autorun 的作用是在可观察数据被修改之后,自动去执行依赖可观察数据的行为,这个行为一直就是传入 autorun 的函数。
2.3 when
接收两个函数参数,第一个函数必须根据可观察数据来返回一个布尔值,当该布尔值为 true 时,才会去执行第二个函数,并且只会执行一次。
import { observable, when } from 'mobx'
class Leo {
@observable str = 'leo';
@observable num = 123;
@observable bool = false;
}
let leo = new Leo()
when(() => leo.bool, () => {
console.log('这是true')
})
leo.bool = true
// 这是true
可以看出当 leo.bool 设置成 true 以后,when 的第二个方法便执行了。
注意
-
第一个参数,必须是根据可观察数据来返回的布尔值,而不是普通变量的布尔值。
-
如果第一个参数默认值为 true,则 when 函数会默认执行一次。
2.4 reaction
接收两个函数参数,第一个函数引用可观察数据,并返回一个可观察数据,作为第二个函数的参数。
reaction 第一次渲染的时候,会先执行一次第一个函数,这样 MobX 就会知道哪些可观察数据被引用了。随后在这些数据被修改的时候,执行第二个函数。
import { observable, reaction } from 'mobx'
class Leo {
@observable str = 'leo';
@observable num = 123;
@observable bool = false;
}
let leo = new Leo()
reaction(() => [leo.str, leo.num], arr => {
console.log(arr)
})
leo.str = 'pingan'
leo.num = 122
// ["pingan", 122]
// ["pingan", 122]
这里我们依次修改 leo.str 和 leo.num 两个变量,会发现 reaction 方法被执行两次,在控制台输出两次结果 ["pingan", 122] ,因为可观察数据 str 和 num 分别被修改了一次。
实际使用场景
:
当我们没有获取到数据的时候,没有必要去执行存缓存逻辑,当第一次获取到数据以后,就执行存缓存的逻辑。
2.5 小结
computed 可以将多个可观察数据组合成一个可观察数据;
autorun 可以自动追踪所引用的可观察数据,并在数据发生变化时自动触发;
when 可以设置自动触发变化的时机,是 autorun 的一个变种情况;
reaction 可以通过分离可观察数据声明,以副作用的方式对 autorun 做出改进;
它们各有特点,互为补充,都能在合适场景中发挥重要作用。
三、MobX 常用 API 介绍
3. 修改可观察数据
在上一部分内容中,我们了解到,对可观察的数据做出反应的时候,需要我们手动修改可观察数据的值。这种修改是通过直接向变量赋值来实现的,虽然简单易懂,但是这样会带来一个较为严重的副作用,就是每次的修改都会触发 autorun 或者 reaction 运行一次。多数情况下,这种高频的触发是完全没有必要的。
比如用户对视图的一次点击操作需要很多修改 N 个状态变量,但是视图的更新只需要一次就够了。
为了优化这个问题, MobX 引入了 action 。
3.1 (@)action
action 是修改任何状态的行为,使用 action 的好处是能将多次修改可观察状态合并成一次,从而减少触发 autorun 或者 reaction 的次数。
可以理解成批量操作,即一次动作中包含多次修改可观察状态,此时只会在动作结束后,做一次性重新计算和反应。
action 也有两种使用方法,这里以 decorate 方式来介绍。
import { observable, computed, reaction, action} from 'mobx'
class Store {
@observable string = 'leo';
@observable number = 123;
@action bar(){
this.string = 'pingan'
this.number = 100
}
}
let store = new Store()
reaction(() => [store.string, store.number], arr => {
console.log(arr)
})
store.bar() // ["pingan", 100]
当我们连续去修改 store.string 和 store.number 两个变量后,再运行 store.bar() 会发现,控制台值输出一次 ["pingan", 100] ,这就说明 reaction 只被执行一次。
知识点:action.bound
另外 action 还有一种特殊使用方法:action.bound,常常用来作为一个 callback 的方法参数,并且执行效果也是一样:
import { observable, computed, reaction, action} from 'mobx'
class Store {
@observable string = 'leo';
@observable number = 123;
@action.bound bar(){
this.string = 'pingan'
this.number = 100
}
}
let store = new Store()
reaction(() => [store.string, store.number], arr => {
console.log(arr)
})
let bar = store.bar;
function foo(fun){
fun()
}
foo(bar) //["pingan", 100]
知识点:runInAction(name?, thunk)
runInAction 是个简单的工具函数,它接收代码块并在(异步的)动作中执行。这对于即时创建和执行动作非常有用,例如在异步过程中。runInAction(f) 是 action(f)() 的语法糖。
import { observable, computed, reaction, action} from 'mobx'
class Store {
@observable string = 'leo';
@observable number = 123;
@action.bound bar(){
this.string = 'pingan'
this.number = 100
}
}
let store = new Store()
reaction(() => [store.string, store.number], arr => {
console.log(arr)
})
runInAction(() => {
store.string = 'pingan'
store.number = 100
})
//["pingan", 100]
四、 Mobx-React 简单实例
这里以简单计数器为例,实现点击按钮,数值累加的简单操作,如图:
在这个案例中,我们引用 mobx-react 库来实现,很明显可以看出 mobx-react 是作为 mobx 和 react 之前的桥梁。
它将 react 组件转化为对可观察数据的反应,也就是将组件的 render 方法包装成 autorun 方法,使得状态变化时能自动重新渲染。
详细可以查看:https://www.npmjs.com/package/mobx-react 。
接下来开始我们的案例:
1. 安装依赖和配置webpack
由于配置和前面第二节介绍差不多,所以这里会以第二节的配置为基础,添加配置。
首先安装 mobx-react 依赖:
cnpm i mobx-react -D
修改webpack.config.js,在 presets 配置中添加 react 进来:
// ... 省略其他
- entry: path.resolve(__dirname, 'src/index.js'),
+ entry: path.resolve(__dirname, 'src/index.jsx'),
module: {
rules: [{
test: /.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
- presets: ['env'],
+ presets: ['env', 'react'],
plugins: ['transform-decorators-legacy', 'transform-class-properties']
}
}
}]
}
2. 初始化 React 项目
这里初始化一下我们本次项目的简单骨架:
// index.jsx
import { observable, action} from 'mobx';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import {observer, PropTypes as observablePropTypes} from 'mobx-react'
class Store {
}
const store = new Store();
class Bar extends Component{
}
class Foo extends Component{
}
ReactDOM.render(<Foo />, document.querySelector("#root"))
这些组件对应到我们最后页面效果如图:
2. 实现 Store 类
Store 类用于存储数据。
class Store {
@observable cache = { queue: [] }
@action.bound refresh(){
this.cache.queue.push(1)
}
}
3. 实现 Bar 和 Foo 组件
实现代码如下:
@observer
class Bar extends Component{
static propTypes = {
queue: observablePropTypes.observableArray
}
render(){
const queue = this.props.queue;
return <span>{queue.length}</span>
}
}
class Foo extends Component{
static propTypes = {
cache: observablePropTypes.observableObject
}
render(){
const cache = this.props.cache;
return <div>
<button onClick={this.props.refresh}>点击 + 1</button>
当前数值:<Bar queue={cache.queue} />
</div>
}
}
这里需要注意:
1.可观察数据类型中的数组,实际上并不是数组类型,这里需要用 observablePropTypes.observableArray 去声明它的类型,对象也是一样。
2.@observer 在需要根据数据变换,而改变UI的组件去引用,另外建议有使用到相关数据的类都引用。
3.事实上,我们只需要记住 observer 方法,将所有 React 组件用 observer 修饰,就是 react-mobx 的用法。
4. 使用 Foo 组件
最后我们使用 Foo 组件,需要给它传递两个参数,这样 Bar 组件才能拿到并使用:
ReactDOM.render(
<Foo cache={store.cache}
refresh={store.refresh}
/>,
document.querySelector("#root")
)
结尾
本文参考:
《MobX 官方文档》