React 是一个专注于视图层的库。React 维护了状态到视图的映射关系,开发者只需关心状态即可,由 React 来操控视图。
在小型应用中,单独使用 React 是没什么问题的。但在复杂应用中,容易碰到一些状态管理方面的问题,如:
React 只提供了在内部组件修改状态的接口
setState
。导致数据、业务逻辑和视图层耦合在组件内部,不利于扩展和维护。React 应用即一颗组件树。兄弟节点,或者不在同一树杈的节点之间的状态同步是非常麻烦。
关心性能的情况下,需要手动设置
shouldComponentUpdate
这时就需要引入状态管理库。现在常用的状态管理库有 Mobx 和 Redux,本文会重点介绍 Mobx,然后会将 Mobx 和 Redux 进行对比,最后展望下未来的 React 状态管理方面趋势。
Mobx 简介
Mobx 的理念非常简单,可以用一个 demo 就把其核心原理说清楚。Mobx/MobxReact 中有三个核心概念,observable
、observer
、action
。为了简单起见,本文没有提及 computed
等概念。
observable
: 通过observable(state)
定义组件的状态,包装后的状态是一个可观察数据(Observable Data)。observer
: 通过observer(ReactComponent)
定义组件。action
: 通过action
来修改状态。
简化图如下:
只讲概念还比较模糊,下面给大家举个例子。
点击运行 https://jsfiddle.net/jhwleo/1L5jcykr/9/
// 通过 observable 定义组件的状态
const user = mobx.observable({
name: "Jay",
age: 22
})
// 通过 action 定义如何修改组件的状态
const changeName = mobx.action(name => user.name = name)
const changeAge = mobx.action(age => user.age = age)
// 通过 observer 定义 ReactComponent 组件。
const Hello = mobxReact.observer(class Hello extends React.Component {
componentDidMount(){
// 视图层通过事件触发 action
changeName('Wang') // render Wang
}
render() {
// 渲染
console.log('render',user.name);
return <div>Hello,{user.name}!</div>
}
})
ReactDOM.render(<Hello />, document.getElementById('mount'));
// 非视图层事件触发,外部直接触发 action
changeName('Wang2')// render Wang2
// 重点:没有触发重新渲染
// 原因:Hello 组件并没有用到 `user.age` 这个可观察数据
changeAge('18') // no console
例子看完了,是不是非常简单。
使用 Mobx,组件状态可以在外部定义(也可以在组件内部),因此,数据、业务逻辑可以轻易地和视图层分离,提高应用的可扩展性和可维护性。另外,由于组件状态可以在外部定义,兄弟节点之间的状态同步也非常容易。最后一点, Mobx 知道什么时候应该渲染页面,因此基本不需要手动设置 shouldComponentUpdate
来提高应用性能。
接下来给大家介绍下 Mobx 中 observable
observer
action
的用法,并会简单介绍一下其原理。
observable
Mobx 如此简单的原因之一,就是使用了可观察数据(Observable Data)。简单说,可观察数据就是可以观察到数据的读取、写入,并进行拦截。
Mobx 提供了 observable
接口来定义可观察数据。定义的可观察数据,通常也是组件的状态。该方法接收一个参数,参数可以是原始数据类型、普通 Object、Array、或者 ES6 中的 Map 类型,返回一个 observable
类型的参数。
Array.isArray(mobx.observable([1,2,3])) === false // true
mobx.isObservable(mobx.observable([1,2,3])) === true // true
注意,数组经过 observable
包装后,就不是 Array 类型了,而是 Mobx 定义的一个特殊类型 ———— observable
类型。observable
类型,可以通过 mobx.isObservable
来检查。
虽然数据类型不一样,但是使用方式基本和原来一致(原始数据类型除外)。
const observableArr = mobx.observable([1,2,3]);
const observableObj = mobx.observable({name: 'Jay'});
const observableMap = mobx.observable(new Map([['name','Wang']]));
console.log(observableArr[0]) // 1
console.log(observableObj.name) // Jay
console.log(observableMap.get('name')) // Wang
可观察数据类型的原理是,在读取数据时,通过 getter
来拦截,在写入数据时,通过setter
来拦截。
Object.defineProperty(o, key, {
get : function(){
// 收集依赖的组件
return value;
},
set : function(newValue){
// 通知依赖的组件更新
value = newValue
},
});
在可观察数据被组件读取时,Mobx 会进行拦截,并记录该组件和可观察数据的依赖关系。在可观察数据被写入时,Mobx 也会进行拦截,并通知依赖它的组件重新渲染。
observer
observer
接收一个 React 组件作为参数,并将其转变成响应式(Reactive)组件。
// 普通组件
const Hello = mobxReact.observer(class Hello extends React.Component {
render() {
return <div>Hello,{user.name}!</div>
}
})
// 函数组件
const Hello = mobxReact.observer( () => (
<div>Hello,{user.name}!</div>
))
响应式组件,即当且仅当组件依赖的可观察数据发生改变时,组件才会自动响应,并重新渲染。
在本文最开始的例子中,响应式组件依赖了 user.name
,但是没有依赖 user.age
。所以当user.name
发现变化时,组件更新。而 user.age
发生变化时,组件没有更新。
这里再详细分析本文中的第一个例子:
user.name = 'Wang2'// render Wang2
// 重点:没有触发重新渲染
// 原因:Hello 组件并没有用到 `user.age` 这个可观察数据
user.age = '18' // no console
当可观察数据变化时,Mobx 会调用 forceUpdate
直接更新组件。
而在传统 React 应用中,当状态、属性变化后会先调用 shouldComponentUpdate
,该方法会深层对比前后状态和属性是否发生改变,再确定是否更新组件。
shouldComponentUpdate
是很消耗性能的。Mobx 通过可观察数据,精确地知道组件是否需要更新,减少了调用 shouldComponentUpdate
这一步。这是 Mobx 性能好的原因之一。
另外需要注意的是 observer
并不是 mobx
的方法,而是 mobx-react
的方法。mobx
和 mobx-react
关系如同 react
与 react-dom
。
action
在 Mobx 中是可以直接修改可观察数据,来进行更新组件的,但不建议这样做。如果在任何地方都修改可观察数据,将导致页面状态难以管理。
所有对可观察数据地修改,都应该在 action
中进行。
const changeName = mobx.action(name => user.name = name)
使用 Mobx 可以将组件状态定义在组件外部,这样,组件逻辑和组件视图便很容易分离,兄弟组件之间的状态也很容易同步。另外,也不再需要手动使用 shouldComponentUpdate
进行性能优化了。
Mobx 与 Redux 对比
Mobx 的优势来源于可变数据(Mutable Data)和可观察数据 (Observable Data) 。
Redux 的优势来源于不可变数据(Immutable data)。
可观察数据的优势,在前文已经介绍过了。现在再来聊聊可变数据和不可变数据。
顾名思义,可变数据和不可变数据的区别在于,可变数据创建后可以修改,不可变数据创建后不可以修改。
可变数据,可以直接修改,所以操作起来非常简单。这使得使用 mobx 改变状态,变得十分简单。
不可变数据并不一定要用到 Immutable 库。它完全可以是一种约定,只要创建后不修改即可。比如说,Redux 中的 state
。每次修改都会重新生成一个 newState
,而不会对原来的值进行改变。所以说 Redux 中的 state
就是不可变数据。
reducer(state, action) => newState.
不可变数据的优势在于,它可预测,可回溯。示例代码如下:
function foo(bar) {
let data = { key: 'value' };
bar(data);
console.log(data.key); // 猜猜会打印什么?
}
如果是可变数据,data.key
的值可能会在 bar
函数中被改变,所以不能确定会打印什么值。但是如果是不可变数据,那么就可以肯定打印值是什么。这就是不可变数据的优势 ———— 可预测。不可变数据不会随着时间的变化(程序的运行)而发生改变。在需要回溯的时候,直接获取保存的值即可。
Mobx 与 Redux 技术选型的本质,是在可变数据与不可变数据之间选择。具体业务场景的技术选型,还需要根据实际情况进行分析,脱离业务场景讨论技术选型是没有意义的。但我个人在状态管理的技术选型上,还是倾向于 Mobx 的。原因是前端与副作用打交道非常频繁,有 Http 请求的副作用,Dom 操作的副作用等等。使用不可变数据,还必须得使用中间件对副作用封装;在 Redux 中修改一次状态,需要经过 Action、Dispatch、Reducer 三个步骤,代码写起来太啰嗦;而前端的程序以中小型程序为主,纯函数带来的可预测性的收益,远不及其带的代码复杂度所需要付出的成本。而 Mobx 使用起来更加简单,更适合现在以业务驱动、快速迭代的开发节奏。
展望:Mobx 与不可变数据的融合
不可变数据和可变数据,都是对状态的一种描述。那么有没有一种方案,能将一种状态,同时用可变数据和不可变数据来描述呢?这样就可以同时享有二者的优势了。(注意:当我们说可变数据时,通常它还是可观察数据,后文统一只说可变数据。)
答案是肯定的,它就是 MST(mobx-state-tree) https://github.com/mobxjs/mob...。
MST 是一个状态容器:一种状态,同时包含了可变数据、不可变数据两种不同的形式。
为了让状态可以在可变数据和不可变数据两种形式之间能够高效地相互转化,必须遵循 MST 定义状态的方法。
在 MST 中,定义状态必须先定义它的结构。状态的结构是一颗树(tree),树是由多层模型(model)组成,model 是由多个节点组成。
在下面的代码中,树只有一层 model,该 model 也只有一个节点:title。title 的类型是事先定好的,在这里是 types.string
。树的结构定义好后,通过 create
方法传入数据,就生成树。
import {types} from "mobx-state-tree"
// declaring the shape of a node with the type `Todo`
const Todo = types.model({
title: types.string
})
// creating a tree based on the "Todo" type, with initial data:
const coffeeTodo = Todo.create({
title: "Get coffee"
})
在一些稍微复杂的例子中,树的 model 可以有多层,每层可以有多个节点,有些节点定义的是数据类型(types.xxx
),有些节点直接定义的是数据。下面的示例中,就是定义了一个多层多节点的树。除此之外,注意 types.model
函数的第一个参数定义的是 model 的名字,第二参数定义的是 model 的所有属性,第三个参数定义的是 action。
import { types, onSnapshot } from "mobx-state-tree"
const Todo = types.model("Todo", {
title: types.string,
done: false
}, {
toggle() {
this.done = !this.done
}
})
const Store = types.model("Store", {
todos: types.array(Todo)
})
// create an instance from a snapshot
const store = Store.create({ todos: [{
title: "Get coffee"
}]})
最关键的来了,请看下面的代码。
// listen to new snapshots
onSnapshot(store, (snapshot) => {
console.dir(snapshot)
})
// invoke action that modifies the tree
store.todos[0].toggle()
// prints: `{ todos: [{ title: "Get coffee", done: true }]}`
在上述代码的第一部分,使用 onSnapshot
监听状态的改变。第二部分,调用 store.todos[0].toggle()
,在这个 action
中通过使用可变数据的方式,直接修改了当前的状态。同时在 onSnapshot
生成了一个状态快照。这个状态快照就是状态的不可变数据的表现形式。
MST 这么神奇,那么具体怎么用呢?MST 只是一个状态容器,同时包含了可变数据和不可变数据。你可以用 MST 直接搭配 React 使用。可以 MST + Mobx + React 配合着用,还可以 MST + Redux + React 混搭着用。
MST 比较新,业内的实践非常少,如果不是急需,现在还可以先观望一下。