五、React 状态管理
5.1 组件状态
React 其实就是这样一个公式
UI = f(data)
f 的参数 data,除了 props,就是 state。props 是组件外传递进来的数据,state 代表的就是 React 组件的内部状
为什么要了解 React 组件自身状态管理
- 因为 React 组件自身的状态管理是基础,其他第三方工具都是在这个基础上构筑的,连基础都不了解,无法真正理解第三方工具
- 对于很多应用场景,React 组件自身的状态管理就足够解决问题,犯不上动用 Redux 和 MobX 这样的大杀器,简单问题简单处理,可以让代码更容易维护
组件自身状态 state
什么数据放在 state 中
对于 React 组件而言,数据分为两种
props
state
二者的区别显而易见,简单说就是,props 是外部传给组件的数据,而 state 是组件自己维护的数据,对外部是不可见的。
所以,判断某个数据以 props 方式存在,还是以 state 方式存在,并不难,只需要判断这个状态是否是组件内部状态。
一个经常被问到的问题,就是为什么不把组件的数据直接存放在组件类的成员变量中?比如像下面这样:
class Foo extends React.Component {
foo = 'foo'
render() {
return (
<React.Fragment>{this.foo}</React.Fragment>
);
}
}
像上面,数据存在 this.foo 中,而不是存在 this.state.foo 中,当这个组件渲染的时候,当然 this.foo 的值也就被渲染出来了,问题是,更新 this.foo 并不会引发组件的重新渲染,这很可能不是我们想要的。
所以,判断一个数据应该放在哪里,用下面的原则
- 如果数据由外部传入,放在 props 中
- 如果是组件内部状态,是否这个状态更改应该立刻引发一次组件重新渲染?如果是,放在 state 中;不是,放在成员变量中
修改 state 的正确方式
组件自身的状态可以通过 this.state 读到,this.state 本身就是一个对象,但是修改状态不应该通过直接修改 this.state 对象来完成。因为,我们修改 state,当然不只是想修改这个对象的值,而是想引发 React 组件的重新渲染。
this.state.foo = 'bar'; //错误的方式
this.setState({foo:'bar'}); //正确的方式
如上面代码所示,如果只是修改 this.state,那改了也就只是改了这个对象,其他的什么都不会发生;如果使用 setState 函数,那不光修改 state,还能引发组件的重新渲染,在重新渲染中就会使用修改后的 state,这也就是达到根据 state 改变公式左侧 UI 的目的。
UI = f(state)
state 改变引发重新渲染的时机
现在我们知道应该用 setState 函数来修改组件 state,而且可以引发组件重新渲染,有意思的是,并不是一次 setState 调用肯定会引发一次重新渲染。
这是 React 的一种性能优化策略,如果 React 对每一次 setState 都立刻做一次组件重新渲染,那代价有点大,比如下面的代码:
this.setState({count: 1});
this.setState({caption: 'foo'});
this.setState({count: 2});
连续的同步调用 setState,第三次还覆盖了第一次调用的效果,但是效果只相当于调用了下面这样一次
this.setState({count: 2, caption: 'foo'});
虽然明智的开发者不会故意连续写三个 setState 调用,但是代码一旦写得复杂,可能有多个 setState 分布在一次执行的不同代码片段中,还是会同步连续调用 setState,这时候,如果真的每个 setState 都引发一次重新渲染,实在太浪费了。
React 非常巧妙地用任务队列解决了这个问题,可以理解为每次 setState 函数调用都会往 React 的任务队列里放一个任务,多次 setState 调用自然会往队列里放多个任务。React 会选择时机去批量处理队列里执行任务,当批量处理开始时,React 会合并多个 setState 的操作,比如上面的三个 setState 就被合并为只更新 state 一次,也只引发一次重新渲染。
因为这个任务队列的存在,React 并不会同步更新 state,所以,在 React 中,setState 也不保证同步更新 state 中的数据。
state 不会被同步修改
简单说来,调用 setState 之后的下一行代码,读取 this.state 并不是修改之后的结果
console.log(this.state.count);// 修改之前 this.state.count 为 0
this.setState({count: 1})
console.log(this.state.count);// 在这里 this.state.count 依然为 0
这乍看是很让人费解的结果,但是如果你理解了上面 React 任务队列的设计,一切也不难理解。
setState 只是给任务队列里增加了一个修改 this.state 的任务,这个任务并没有立即执行,所以 this.state 并不会立刻改变。
好吧,其实问题也没有那么简单,上面我所举的例子中,都假设 setState 是由 React 的生命周期函数或者事件处理函数中同步调用,这种情况下 setState 不会立即同步更新 state 和重新渲染,但是,如果调用 setState 由其他条件引发,就不是这样了。
看下面的代码,结果可能会出乎你的所料:
setTimeout(() => {
this.setState({count: 2}); //这会立刻引发重新渲染
console.log(this.state.count); //这里读取的 count 就是 2
}, 0);
为什么 setTimeout 能够强迫 setState 同步更新 state 呢?
可以这么理解,当 React 调用某个组件的生命周期函数或者事件处理函数时,React 会想:嗯,这一次函数可能调用多次 setState,我会先打开一个标记,只要这个标记是打开的,所有的 setState 调用都是往任务队列里放任务,当这一次函数调用结束的时候,我再去批量处理任务队列,然后把这个标记关闭。
因为 setTimeout 是一个 JavaScript 函数,和 React 无关,对于 setTimeout 的第一个函数参数,这个函数参数的执行时机,已经不是 React 能够控制的了,换句话说,React 不知道什么时候这个函数参数会被执行,所以那个标记
也没有打开。
当那个标记
没有打开时,setState 就不会给任务列表里增加任务,而是强行立刻更新 state 和引发重新渲染。这种情况下,React 认为:这个 setState 发生在自己控制能力之外,也许开发者就是想要强行同步更新呢,宁滥勿缺,那就同步更新了吧。
知道这个技巧
之后,可能会有开发者说:好啊,那么以后我就用 setTimeout 来调用 setState 吧,能够立刻更新 state,多好!
我劝你不要这么做。
就像上面所说,React 选择不同步更新 state,是一种性能优化,如果你用上 setTimeout,就没机会让 React 优化了。
而且,每当你觉得需要同步更新 state 的时候,往往说明你的代码设计存在问题,绝大部分情况下,你所需要的,并不是state 立刻更新
,而是,确定 state 更新之后我要做什么
,这就引出了 setState 另一个功能
setState 的第二个参数
setState 的第二个参数可以是一个回调函数,当 state 真的被修改时,这个回调函数会被调用
console.log(this.state.count); // 0
this.setState({count: 1}, () => {
console.log(this.state.count); // 这里就是 1 了
})
console.log(this.state.count); // 依然为 0
当 setState 的第二个参数被调用时,React 已经处理完了任务列表,所以 this.state 就是更新后的数据。
如果需要在 state 更新之后做点什么,请利用第二个参数。
函数式 setState
不管怎么说,setState 不能同步更新的确会带来一些麻烦,尤其是多个 setState 调用之间有依赖关系的时候,很容易写错代码。
一个很典型的例子,当我们不断增加一个 state 的值时:
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
上面的代码表面上看会让 this.state.count 增加 3,实际上只增加了 1,因为 setState 没有同步更新 this.state 啊,所以给任务队列加的三个任务都是给 this.state.count 同一个值而已。
面对这种情况,我们很自然地想到,如果任务列表中的任务不只是给 state 一个固定数据,如果任务列表里的任务
是一个函数,能够根据当前 state 计算新的状态,那该多好!
实际上,setState 已经支持这种功能,到现在为止我们给 setState 的第一个参数都是对象,其实也可以传入一个函数。
当 setState 的第一个参数为函数时,任务列表上增加的就是一个可执行的任务函数了,React 每处理完一个任务,都会更新 this.state,然后把新的 state 传递给这个任务函数。
setState 第一个参数的形式如下:
function increment(state, props) {
return {count: state.count + 1};
}
可以看到,这是一个纯函数,不光接受当前的 state,还接受组件的 props,在这个函数中可以根据 state 和 props 任意计算,返回的结果会用于修改 this.state。
如此一来,我们就可以这样连续调用 setState:
this.setState(increment);
this.setState(increment);
this.setState(increment);
用这种函数式方式连续调用 setState,就真的能够让 this.state.count 增加 3,而不只是增加 1。
5.2 Mobx 使用模式
理解 Mobx
虽然 Mobx 和 Redux 有很大不同,但是至少还有一个共同点——这两个工具都和 React 没有任何直接关系,只不过凑巧 React 社区大量使用它们罢了。从技术上说,Mobx 和 Redux 都是中立的状态管理工具,他们能够应用于 React,也可以用于其他需要状态管理的场景
我们用 Mobx 来实现一个很简单的计数工具,首先,需要有一个对象来记录计数值,代码如下:
import {observable} from 'mobx';
const counter = observable({
count: 0
});
在上面的代码中,counter 是一个对象,其实就是用 observable 函数包住一个普通 JavaScript 对象,但是 observable 的介入,让 counter 对象拥有了神力。
我们用最简单的代码来展示这种神力
,代码如下:
import {autorun} from 'mobx';
window.counter = counter;
autorun(() => {
console.log('#count', counter.count);
});
把 counter 赋值给 window.counter,是为了让我们在 Chrome 的开发者界面可以访问。用 autorun 包住了一个函数,这个函数输出 counter.count 的值,这段代码的作用,我们很快就能看到。
在 Chrome 的开发者界面,我们可以直接访问 window.counter.count,神奇之处是,如果我们直接修改 window.counter.count 的值,可以直接触发 autorun 的函数参数!
这个现象说明,mobx 的 observable 拥有某种神力
,任何对这个对象的修改,都会立刻引发某些函数被调用。和 observable 这个名字一样,被包装的对象变成了被观察者
,而被调用的函数就是观察者
,在上面的例子中,autorun 的函数参数就是观察者
。
Mobx 这样的功能,等于实现了设计模式中的观察者模式
(Observer Pattern),通过建立 observer 和 observable 之间的关联,达到数据联动。不过,传统的观察者模式
要求我们写代码建立两者的关联,也就是写类似下面的代码:
observable.register(observer);
Mobx 最了不起之处,在于不需要开发者写上面的关联代码,Mobx 自己通过解析代码就能够自动发现 observer 和 observable 之间的关系。
我们很自然想到,如果让我们的数据拥有这样的神力
,那我们就不用在修改完数据之后,再费心去调用某些函数使用这些数据了,数据管理会变得十分轻松。
decorator
因为 Mobx 的作用就是把简单的对象赋予神力,总要有一种方法能够在不改变对象代码的前提,去改变对象的行为,这就用得上装饰者模式
(Decorator Pattern)。
单独说装饰者模式
,这只是面向对象编程思想下的一种模式,不过对 JavaScript 语言而言,就不只是一种模式,而是一种语言特性,它在语法上对这种模式提供了强大的支持,所谓强大,就是指使用起来代码极其简洁。
根据 JavaScript 语法,我们可以这样创造一个 decorator,叫做 log:
function log(target, name, descriptor) {
console.log('#target', target);
console.log('#name', name);
console.log('#descriptor', descriptor);
return descriptor;
}
当然,很明显这个 decorator 什么实质的事情都没做,只是用 console.log 输出了三个参数秀了一下存在感,最后返回的 descriptor,就是被这个『装饰者』所『装饰』的对象。
下面是使用这个 decorator 的代码示例:
@log
class Bar {
@log
bar() {
console.log('bar');
}
}
可以看到,@ 符号就是使用 decorator 的标志,将 @log 作用于一个类 Bar,那么最后得到的 Bar 其实是调用 log 函数返回的结果;将 @log 作用于一个类成员 @bar,最后得到的 bar 同样是调用 log 函数之后得到的结果。可见,如果我们巧妙地编写 log 函数,控制返回的结果,就可以操纵被『装饰』的类或者成员。
编写 decorator 是一个复杂的过程,也超出了这本小册的范围,有兴趣的读者可以自行研究。在这里,读者只需要知道,虽然使用 Mobx 并不是必须使用 decorator,但是使用 decorator 会让 Mobx 的应用代码简洁易读很多
用 decorator 来使用 Mobx
还是以 Counter 为例,看如何用 decorator 使用 Mobx,我们先看代码:
import {observable} from 'mobx';
import {observer} from 'mobx-react';
@observer
class Counter extends React.Component {
@observable count = 0;
onIncrement = () => {
this.count ++;
}
onDecrement = () => {
this.count --;
}
componentWillUpdate() {
console.log('#enter componentWillUpdate');
}
render() {
return(
<CounterView
caption="With decorator"
count={this.count}
onIncrement={this.onIncrement}
onDecrement={this.onDecrement}
/>
);
}
}
在上面的代码中,Counter 这个 React 组件自身是一个 observer,而 observable 是 Counter 的一个成员变量 count。
注意 observer 这 个 decorator 来自于 mobx-react,它是 Mobx 世界和 React 的桥梁,被它装饰
的组件,只要用到某个被 Mobx 的 observable 装饰
过的数据,自然会对这样的数据产生反应。所以,只要 Counter 的 count 成员变量一变化,就会引发 Counter 组件的重新渲染。
可以注意到,Counter 的代码中并没有自己的 state,其实,被 observer 修饰过之后,Counter 被强行注入了 state,只不过我们看不见而已。
独立的 Store
虽然把 observer 和 observable 集中在一个 React 组件中可行,但是,这也让 observable 的状态被封存在了 React 组件内,那我们直接用 React 自身的 state 管理也能解决问题,所以,这样使用 Mobx 意义不大。
更多适用于 Mobx 的场合,就和适用于 Redux 的场合一样,是一个状态需要多个组件共享,所以 observable 一般是在 React 组件之外。
我们重写一遍 Counter 组件,代码如下:
const store = observable({
count: 0
});
store.increment = function() {
this.count ++;
};
store.decrement = function() {
this.count --;
}
@observer // this decorator is must
class Counter extends React.Component {
onIncrement = () => {
store.increment();
}
onDecrement = () => {
store.decrement();
}
render() {
return(
<CounterView
caption="With external state"
count={store.count}
onIncrement={this.onIncrement}
onDecrement={this.onDecrement}
/>
);
}
}
可以看到,我们把 count 提到组件之外,甚至就把它叫做 store,这延续的是 Redux 的命名方法
总结
- Mobx 的基本功能就是
观察者模式
- decorator 是
装饰者模式
在 JavaScript 语言中的实现 - Mobx 通常利用 decorator 来使用
5.3 不同方式对比
Mobx 和 Redux 的比较
Mobx 和 Redux 的目标都是管理好应用状态,但是最根本的区别在于对数据的处理方式不同。
Redux 认为,数据的一致性很重要,为了保持数据的一致性,要求 Store 中的数据尽量范式化,也就是减少一切不必要的冗余,为了限制对数据的修改,要求 Store 中数据是不可改的(Immutable),只能通过 action 触发 reducer 来更新 Store。
Mobx 也认为数据的一致性很重要,但是它认为解决问题的根本方法不是让数据范式化,而是不要给机会让数据变得不一致。所以,Mobx 鼓励数据干脆就反范式化
,有冗余没问题,只要所有数据之间保持联动,改了一处,对应依赖这处的数据自动更新,那就不会发生数据不一致的问题。
值得一提的是,虽然 Mobx 最初的一个卖点就是直接修改数据,但是实践中大家还是发现这样无组织无纪律不好,所以后来 Mobx 还是提供了 action 的概念。和 Redux 的 action 有点不同,Mobx 中的 action 其实就是一个函数,不需要做 dispatch,调用就修改对应数据,在上面的代码中,increment 和 decrement 就是 action。
如果想强制要求使用 action,禁止直接修改 observable 数据,使用 Mobx 的 configure,如下:
import {configure} from 'mobx';
configure({enforceActions: true});
总结一下 Redux 和 Mobx 的区别,包括这些方面:
- Redux 鼓励一个应用只用一个 Store,Mobx 鼓励使用多个 Store;
- Redux 使用
拉
的方式使用数据,这一点和 React 是一致的,但 Mobx 使用推
的方式使用数据,和 RxJS 这样的工具走得更近; - Redux 鼓励数据范式化,减少冗余,Mobx 容许数据冗余,但同样能保持数据一致。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论