背景
在 2013 年以前,对于数据流的控制一直以来是使用的 MVC(Model-View-Controller) 架构模式来进行数据的管理:
- Model(模型) 负责管理数据;
- View(视) 负责渲染用户界面;
- Controller(控制器) 负责接受用户的输入,根据用户输入调用对应 Model 部分逻辑,把产生的数据结果交给 View 部分,让 View 渲染出必要的输出;
图1-理想的MVC
但是对于非常巨大的代码库和庞大的组织来说,MVC 很快就会变得非常复杂。每当工程师需要新增一个功能的时候,对代码的修改可能带来新的 bug,不同模块之间的依赖关系会变得“脆弱而且不可预测”。如图2:
图2 - 现实的MVC
基于以上的情况,Facebook 公司推出了 Flux 框架,用来管理数据,相比于 MVC ,它是一种更严格的数据流控制。
图3 - Flux框架
一个 Flux 包含四个部分,如下:
- Dispatcher:处理分发动作,维持Store之间的依赖关系;
- Store:负责存储数据和处理数据相关的逻辑;
- Action:驱动 Dispatcher;
- View:视图展示;
当用户请求一个动作,会触发 Action,之后 Action e驱动 Dispatcher 来进行分发 Action 操作,从而更新 Store 中的数据,Store 中数据改变后,就会更新 View 的展示。
Flux 虽然很好,也有不足之处,比如说 难以进行服务端渲染、Store 混杂了逻辑和状态等,但是这种 单一数据流 的理念衍生出了像 Redux 和 MobX 框架的实现。本篇文章着重讲述 MobX。
简介
MobX 通过 响应式编程(在命令式编程环境中,a := b + c 表示将表达式的结果赋给 a,而之后改变 b 或者 c 的值不会影响 a 的值,但在响应式编程中, a 的值会随着 b 或 c 的值得改变而改变)的思想来管理数据。MobX 也是支持单向数据流的,是通过 action 触发 state 的变化,进而触发 state 的衍生对象(Computed 和 Reactions)。所有的衍生默认都是同步更新的。
图4 - MobX实现
概念
装饰器
ESNext 中新增了 decorator 属性,所谓装饰器,可以简单的理解为 锦上添花;以钢铁侠为例,钢铁侠本质上是一个人,只是装饰了很多的武器以后才变得很 NB ,不过怎么装饰他还是一个人。
图5 - 钢铁侠装饰器
function decorateArmour(target, key, descriptor) {
const method = descriptor.value;
let moreDef = 100;
let ret;
descriptor.value = (...args)=>{
args[0] += moreDef;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour //这个就是使用了装饰器
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
toString(){
return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
}
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3
tips:
ES7 中的 decorator 其实是一个语法糖,不过依赖于 Object.defineProperty(obj, prop, descriptor)
- obj : 要在其上定义属性的对象;
- prop: 要定义和修改的属性的名称;
- descriptor: 将被定义或者修改的属性的描述符;
可观察数据
在 MobX 中, State 就对应业务的原始状态,可以通过 observable 或者 @observable 将这些状态变为可观察的,顾名思义,可观察数据就是 当数据变化的时候,可以被观察到。
- 哪些数据可以被观察
一般来说,原始类型(String, Number, Boolean, Symbol),对象(objects),数组(arrays),maps 都可以被观察,其中 objects arrays maps 用 observable 转换为可观察数据,而 原始数据 用 observable.box 转化为可观察数据;
-
普通object
普通对象和非普通对象的划分就是看对象是否有原型,如果没有原型或者原型是Object.prototype 的对象,那么就是普通对象;
const person = observable({
name: 'lily',
age: 26,
address: {
province: '天津',
}
});
console.log('打印经过observable修饰后的对象---', person);
//Proxy {Symbol(mobx administration): ObservableObjectAdministration$$1}
// 默认第一次会执行一次
autorun(() => {
console.log(`name: ${person.name}-- age:${person.age}--address:${JSON.stringify(person.address)}`);
});
// 改变name的值
person.name = '石丽丽';
//name: 石丽丽-- age:26--address:{"province":"天津"}
// 改变 address 的province的值, 说明会递归遍历整个对,即使属性还是个对象;
person.address.province = '北京';
//name: 石丽丽-- age:26--address:{"province":"北京"}
// 新增的属性是不可以被观察的,可以使用 extendObservable
person.obj = "web developer";
// 没有打印的结果
// name: 石丽丽-- age:26--address:{"province":"北京"}--obj:web developer--extendObj:undefined
// 使用 extendObservable
extendObservable(person, {
extendObj: 'extend web developer'
})
// name: 石丽丽-- age:26--address:{"province":"北京"}--obj:web developer--extendObj:extend web developer
tips: 对于新增的属性,不可以被观察,如果需要被观察需要用 extendObservable或者set;observable 会递归遍历整个对象,即使这个属性还是个对象;
-
非普通对象
observable 会返回一个特殊的boxed values 类型的可观测的对象,返回的 boxed values 对象并不会把非普通对象的属性转换为可观测的,而是保存一个指向对象的引用;这个引用是可观测的;对原对象的访问和修改可以通过 get() 和 set() 方法操作。
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = observable.box(new Person('lily', 26));
autorun(() => {
console.log(`name: ${person.get().name}, age: ${person.get().age}`);
});
// 改变属性
person.get().name = 'wanghong';
// 不会打印
// 改变引用
person.set(new Person('wanghong', 27));
//name: wanghong, age: 27
对于非普通的对象的属性,可以通过以下的方式将其变为可观察的:
//将非普通对象的属性变为可观察的
function Person(name, age) {
// 使用 extendObservable 在构造函数中创建可观察的属性
extendObservable(this, {
name: name,
age: age,
})
}
const person = new Person('extendlily', 28);
autorun(() => {
console.log(`使用extendObservable创建可观察的属性--name:${person.name},age:${person.age}`);
});
// 改变对象的属性
person.name = '王宏';
// 使用extendObservable创建可观察的属性--name:王宏,age:28
这种方式比较麻烦,所以推荐使用装饰器的方式,这种方式的好处还在于对于原始数据类型的数据的话,自己内部有判断,不用使用observable.box()如下:
// 非常推荐的一种方式,使用 @observable 装饰器
class Person {
@observable name;
@observable age;
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const person = new Person('@lily', 20);
autorun(() => {
console.log(`使用装饰器修饰的--name:${person.name},age:${person.age}`);
})
// 改变可观察的属性
person.name = 'wanghong';
// 使用装饰器修饰的--name:wanghong,age:20
-
arrays
const arr = observable([1,2,3]);
console.log(`用observable修饰的数组`, arr);
autorun(() => {console.log(`arr--, ${arr}`)})
// 判断是不是一个数组
arr.push(4);
// arr--, 1,2,3,4
tips: 判断是不是数数组的两种方式: Array.isArray(observable([]).slice()) 和 isArrayLike(arr);
-
maps
const map = observable.map({ key: "value" });
autorun(() => {
console.log(`map:${map.get('key')}`);
})
// 改变key的值
map.set("key", "new value");
// map:new value
tips: Map 对象的每个对象都是可观测的,而且向Map对象中添加或删除元素的行为也是可以被观测的;
-
原始数据类型
对于原始数据的话,通过 get() 获取数据,通过 set() 设置数据;
const cityName = observable.box('Vienna');
console.log(cityName.get()); // Vienna
对 observables 做出响应
MobX 中四种方式对 observables 做出响应,分别为 @computed autorun when reaction,接下来会分别介绍这四种方式的使用场景:
-
@computed
Computed values are values that can be derived from the existing state or other computed values. Conceptually, they are very similar to formulas in spreadsheets. Computed values can't be underestimated, as they help you to make your actual modifiable state as small as possible. Besides that they are highly optimized, so use them wherever possible.
计算值(computed values)是可以根据现有的状态或其它计算值衍生出的值。 概念上来说,它们与excel表格中的公式十分相似。 不要低估计算值,因为它们有助于使实际可修改的状态尽可能的小。 此外计算值还是高度优化过的,所以尽可能的多使用它们。
以上的这句话是 MobX 官网的原话,这段话充分的说明了 MobX 的使用场景和重要性。Mobx 是纯函数,不能改变state的状态,computed value 采用的是延迟更新,computed values are automatically derived from your state if any value that affects them changes。如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。
class Squared{
@observable length = 2;
constructor(length) {
this.length = length;
}
@computed get squared() {
return this.length * this.length;
}
}
const square = new Squared(2);
// 改变长度
square.length = 3;
// Squared的面积:9
autorun(() => {
console.log(`Squared的面积:${square.squared}`)
});
-
autorun
顾名思义,就是自动执行,当使用 autorun 时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发。相比于 computed,他不会产生一个新的值,它更像是发起请求这样的命令式副作用;它会返回一个清楚函数 disposer,当不需要观察相关的 state 变化的时,可以调用 disposer 函数清除副作用。
const number = observable([1,2,3]);
const sum = computed(() => number.reduce((a, b) => a + b), 0);
const disposer = autorun(() => {console.log(sum.get())});
number.push(4);
// 10
// 清除副作用
disposer();
number.push(5);
// 不执行
-
when
when(predicate: () => boolean, effect?: () => void, options?),predicate 会自动响应它使用的任何的state的变化,当predicate 返回ture 的时候,函数effect会执行,且执行一次。when 也返回一个 disposer 函数。when 非常的适合用在以影响式的方式执行取消或者清楚逻辑的场景;
componentDidMount() {
class showDetail{
@observable first = 2;
@observable second = 3;
@computed get isVisible() {
return (this.first * this.second) > 10
}
constructor() {
when(
() => this.isVisible,
() => this.dispose(),
)
}
dispose() {
console.log('这里做清理工作');
}
}
const demo = new showDetail();
demo.second = 10;
// 这里做清理工作
```
- reation
reaction(() => data, (data, reaction) => { sideEffect }, options?),它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入,第二个函数 reaction 会返回一个清楚函数 disposer。第一个函数是返回需要被观察的数据,第二个函数接收这个需要被观察的数据,同时传入 reaction,当被观察的数据改变的时候,就会触发 reaction,这样就不像 autorun 似的,当一个状态改变的时候就会触发,从而建立和要被观察的数据和reaction之间的关系;总结来说,相较于autorun,reaction 可以对跟踪哪些对象有更多的控制;
const todos = observable([
{
title: 'Java',
done: true,
},
{
title: 'javascript',
done: false,
}
]);
// 对 length 做出反应
const resLen = reaction(
() => todos.length,
length => console.log(`对长度做出反应:${todos.map(todo => todo.title).join(',')}`)
)
// 对 length 和 title 的变化作出反应
const resTitle = reaction(
() => todos.map(todo => todo.title),
titles => console.log(`对标题做出反应:${titles.join(',')}`)
);
// autorun 对任何可观察数据做出反应
const resAll = autorun(
() => console.log(`autorun 对任何的变化做出反应: ${todos.map(todo => todo.title).join(',')}`)
)
// 改变todos的长度
todos.push({
title: 'C++',
done: false,
});
/**
* 对长度做出反应:Java,javascript,C++
* Reaction.jsx:26 对标题做出反应:Java,javascript,C++
* Reaction.jsx:31 autorun 对任何的变化做出反应: Java,javascript,C++
* */
// 改变 title
todos[0].title = 'Make tea';
/**
* 对标题做出反应:Make tea,javascript,C++
* Reaction.jsx:31 autorun 对任何的变化做出反应: Make tea,javascript,C++
* */
改变observables
官方建议修改 observables 或者具有副作用的函数使用 @action,简单的说就是对于修改可观察的数据,建议使用 @action。
-
@action 和 @action.bound
action 装饰器/函数遵循 javascript 中标准的绑定规则。但是,action.bound 可以用来自动地将动作绑定到目标对象。 注意,与 action 不同的是,(@)action.bound 不需要一个name参数,名称将始终基于动作绑定的属性。
class Ticker{
@observable tick = 0;
@computed get ifDispose() {
return this.tick >= 10;
}
// 使用@action.bound 绑定的this永远是正确的
@action.bound
// 使用action的话就不会增加,因为此时的this是window
// @action
increment() {
this.tick ++;
}
}
const ticker = new Ticker();
const disposer = autorun(() => {console.log(`tick: ${ticker.tick}`)})
when(
() => ticker.ifDispose,
() => disposer(),
)
setInterval(ticker.increment, 1000)
-
asny actions
action包装/装饰器只会对当前运行的函数作出反应,而不会对当前运行函数所调用的函数(不包含在当前函数之内)作出反应! 这意味着如果 action 中存在 setTimeout、promise 的 then 或 async 语句,并且在回调函数中某些状态改变了,那么这些回调函数也应该包装在 action 中。
mobx-react
mobx-react 顾名思义,是联系 Mobx 和 React 之间的桥梁,从而更方便的使用 Mobx 在 React 中开发,经常的用到有:Provider inject observer/@observer:
手写一个 todoList
开发环境的搭建
MobX 中大量的使用了 ES.Next 中的装饰器语法,为了在新搭建的项目中支持这种语法,有两种实现方式:
-
使用create-react-app project-name --scripts-version custome-react-scripts 创建项目,这种方式创建的项目,支持 修饰器语法、Less、Sass;
-
仍然使用create-react-app project-name 创建项目,然后执行yarn run eject弹射出配置文件,然后安装 yarn add babel-plugin-transform-decorators-legacy -D 修改 webpack 的配置文件,添加
"babel": {
"plugins": [
"transform-decorators-legacy"
],
"presets": [
"react-app"
]
}
以上的内容配置好了以后,还要通过 yarn add mobx-react mobx -S 安装 mobx-react;
目录结构
图6-目录结构
代码
因为代码比较多,所以直接上github的地址:https://github.com/ycshill/shared/tree/master/mobx-share
MobX 常用工具函数和调试
-
toJS(value, options)
递归地将一个observable对象转换为javascript结构。支持observable数组、对象、映射和原始数据类型
const person = observable({
name: 'lily',
age: 26,
});
console.log(`没有时候用toJS转化时候的对象--`, person);
// 没有时候用toJS转化时候的对象-- Proxy {Symbol(mobx administration): ObservableObjectAdministration$$1}
console.log(`通过toJS转化后的对象---`, toJS(person));
// 通过toJS转化后的对象:{"name":"lily","age":26}
-
mobx-react-devtools
mobx-react-devtools 是一个用来调试 MobX + React 项目的工具,可以追踪组件的渲染以及组件依赖的可观测数据。
性能优化
Redux VS Mobx
图8- redux&mobx
- 社区观点
- store
- redux:单一
store,通过拆分reducer来拆分应用逻辑,单一store可以方便不同组件之间进行的数据共享;
- mobx:多个
store,把逻辑拆分到不同的store中,当维护多个组件之间的数据共享、相互之间的引用的时候会变得特别的麻烦。
- 编程思想
- redux:是基于函数式的编程思想;
- mobx: 是面向对象的编程思想;
- state:
- redux:
state不可改变,每次状态的变化,都会创建一个新的state
- mobx:
state是可观测对象,并且state可以被直接的修改,state的变化会自动触发使用它的组件重新渲染。
- 源码观点
推荐一篇文章:我为什么从Redux迁移到了Mobx
- 个人观点
- mobx: 个人感觉可观察数据变化的时候,组件自动更新,只需要在组件上加上
@observer修饰组件就可在修改数据的时候自动进行处理更新,同时免面向对象的编程的写法感觉如果熟悉面向对象编程的开发人员,会减少学习的成本;
- redux:单一
store对于数据的管理,更加容易的跟踪state,但是学些成本和负责的更改数据流程让人觉得不是很友好。
背景
在 2013 年以前,对于数据流的控制一直以来是使用的 MVC(Model-View-Controller) 架构模式来进行数据的管理:
但是对于非常巨大的代码库和庞大的组织来说,MVC 很快就会变得非常复杂。每当工程师需要新增一个功能的时候,对代码的修改可能带来新的 bug,不同模块之间的依赖关系会变得“脆弱而且不可预测”。如图2:
基于以上的情况,Facebook 公司推出了 Flux 框架,用来管理数据,相比于 MVC ,它是一种更严格的数据流控制。
一个 Flux 包含四个部分,如下:
当用户请求一个动作,会触发 Action,之后 Action e驱动 Dispatcher 来进行分发 Action 操作,从而更新 Store 中的数据,Store 中数据改变后,就会更新 View 的展示。
Flux 虽然很好,也有不足之处,比如说 难以进行服务端渲染、Store 混杂了逻辑和状态等,但是这种 单一数据流 的理念衍生出了像 Redux 和 MobX 框架的实现。本篇文章着重讲述 MobX。
简介
MobX 通过 响应式编程(在命令式编程环境中,
a := b + c表示将表达式的结果赋给 a,而之后改变 b 或者 c 的值不会影响 a 的值,但在响应式编程中, a 的值会随着 b 或 c 的值得改变而改变)的思想来管理数据。MobX 也是支持单向数据流的,是通过 action 触发 state 的变化,进而触发 state 的衍生对象(Computed 和 Reactions)。所有的衍生默认都是同步更新的。概念
装饰器
ESNext 中新增了 decorator 属性,所谓装饰器,可以简单的理解为 锦上添花;以钢铁侠为例,钢铁侠本质上是一个人,只是装饰了很多的武器以后才变得很 NB ,不过怎么装饰他还是一个人。
tips:
ES7 中的 decorator 其实是一个语法糖,不过依赖于
Object.defineProperty(obj, prop, descriptor)可观察数据
在 MobX 中, State 就对应业务的原始状态,可以通过
observable或者@observable将这些状态变为可观察的,顾名思义,可观察数据就是 当数据变化的时候,可以被观察到。一般来说,原始类型(String, Number, Boolean, Symbol),对象(objects),数组(arrays),maps 都可以被观察,其中
objectsarraysmaps用observable转换为可观察数据,而 原始数据 用observable.box转化为可观察数据;普通object
普通对象和非普通对象的划分就是看对象是否有原型,如果没有原型或者原型是Object.prototype 的对象,那么就是普通对象;
tips: 对于新增的属性,不可以被观察,如果需要被观察需要用
extendObservable或者set;observable 会递归遍历整个对象,即使这个属性还是个对象;非普通对象
observable 会返回一个特殊的boxed values 类型的可观测的对象,返回的 boxed values 对象并不会把非普通对象的属性转换为可观测的,而是保存一个指向对象的引用;这个引用是可观测的;对原对象的访问和修改可以通过
get()和set()方法操作。对于非普通的对象的属性,可以通过以下的方式将其变为可观察的:
这种方式比较麻烦,所以推荐使用装饰器的方式,这种方式的好处还在于对于原始数据类型的数据的话,自己内部有判断,不用使用
observable.box()如下:arrays
tips: 判断是不是数数组的两种方式:
Array.isArray(observable([]).slice())和isArrayLike(arr);maps
tips: Map 对象的每个对象都是可观测的,而且向Map对象中添加或删除元素的行为也是可以被观测的;
原始数据类型
对于原始数据的话,通过
get()获取数据,通过set()设置数据;对 observables 做出响应
MobX 中四种方式对 observables 做出响应,分别为
@computedautorunwhenreaction,接下来会分别介绍这四种方式的使用场景:@computed
以上的这句话是 MobX 官网的原话,这段话充分的说明了 MobX 的使用场景和重要性。Mobx 是纯函数,不能改变state的状态,computed value 采用的是延迟更新,computed values are automatically derived from your state if any value that affects them changes。如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。
autorun
顾名思义,就是自动执行,当使用 autorun 时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发。相比于 computed,他不会产生一个新的值,它更像是发起请求这样的命令式副作用;它会返回一个清楚函数 disposer,当不需要观察相关的 state 变化的时,可以调用 disposer 函数清除副作用。
when
when(predicate: () => boolean, effect?: () => void, options?),predicate 会自动响应它使用的任何的state的变化,当predicate返回ture 的时候,函数effect会执行,且执行一次。when也返回一个disposer函数。when 非常的适合用在以影响式的方式执行取消或者清楚逻辑的场景;componentDidMount() {
class showDetail{
@observable first = 2;
@observable second = 3;
reaction(() => data, (data, reaction) => { sideEffect }, options?),它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入,第二个函数 reaction 会返回一个清楚函数 disposer。第一个函数是返回需要被观察的数据,第二个函数接收这个需要被观察的数据,同时传入 reaction,当被观察的数据改变的时候,就会触发 reaction,这样就不像autorun似的,当一个状态改变的时候就会触发,从而建立和要被观察的数据和reaction之间的关系;总结来说,相较于autorun,reaction 可以对跟踪哪些对象有更多的控制;改变observables
官方建议修改 observables 或者具有副作用的函数使用
@action,简单的说就是对于修改可观察的数据,建议使用@action。@action 和 @action.bound
asny actions
使用
action这里列出了比较常见的一种方式,就是使用
action进行包装:使用
runInAction工具函数这种模式的优点在于你可以不用到处的写
action,而仅仅在整个过程结束的时候对状态进行修改:flowsmobx-react
mobx-react顾名思义,是联系Mobx和React之间的桥梁,从而更方便的使用Mobx在React中开发,经常的用到有:Providerinjectobserver/@observer:Provider
Provider 是一个 React 组件,利用 Reacxt 的
context机制把应用所需的state传递给子组件。inject
inject 是个高阶组件,和
Provider结合使用,用于从Provider提取所需的state,作为props传递给目标组件。observer/@observer
手写一个 todoList
开发环境的搭建
MobX 中大量的使用了 ES.Next 中的装饰器语法,为了在新搭建的项目中支持这种语法,有两种实现方式:
使用
create-react-app project-name --scripts-version custome-react-scripts创建项目,这种方式创建的项目,支持 修饰器语法、Less、Sass;仍然使用
create-react-app project-name创建项目,然后执行yarn run eject弹射出配置文件,然后安装yarn add babel-plugin-transform-decorators-legacy -D修改 webpack 的配置文件,添加以上的内容配置好了以后,还要通过
yarn add mobx-react mobx -S安装mobx-react;目录结构
代码
因为代码比较多,所以直接上github的地址:https://github.com/ycshill/shared/tree/master/mobx-share
MobX 常用工具函数和调试
toJS(value, options)递归地将一个observable对象转换为javascript结构。支持observable数组、对象、映射和原始数据类型
mobx-react-devtools
mobx-react-devtools 是一个用来调试 MobX + React 项目的工具,可以追踪组件的渲染以及组件依赖的可观测数据。
安装
yarn add mobx-react-devtools -D代码
性能优化
尽可能多地使用小组件
@observer 组件会追踪render方法中所有的可观测的值,当任何一个值变化的时候,都会重新渲染,所以组件越小,重新渲染的变化就越小。
在专用的组件中渲染列表
晚一点使用间接引用值
快的:
<DisplayName person={person} />慢的:
<DisplayName name={person.name} />Redux VS Mobx
store,通过拆分reducer来拆分应用逻辑,单一store可以方便不同组件之间进行的数据共享;store,把逻辑拆分到不同的store中,当维护多个组件之间的数据共享、相互之间的引用的时候会变得特别的麻烦。state不可改变,每次状态的变化,都会创建一个新的statestate是可观测对象,并且state可以被直接的修改,state的变化会自动触发使用它的组件重新渲染。推荐一篇文章:我为什么从Redux迁移到了Mobx
@observer修饰组件就可在修改数据的时候自动进行处理更新,同时免面向对象的编程的写法感觉如果熟悉面向对象编程的开发人员,会减少学习的成本;store对于数据的管理,更加容易的跟踪state,但是学些成本和负责的更改数据流程让人觉得不是很友好。