乐趣区

理解 React 轻量状态管理库 Unstated

在 React 写应用的时候,难免遇到跨组件通信的问题。现在已经有很多的解决方案。

React 本身的 Context
Redux 结合 React-redux
Mobx 结合 mobx-react

React 的新的 Context api 本质上并不是 React 或者 Mbox 这种状态管理工具的替代品,充其量只是对 React 自身状态管理短板的补充。而 Redux 和 Mbox 这两个库本身并不是为 React 设计的,对于一些小型的 React 应用比较重。
基本概念
Unstated 是基于 context API。也就是使用 React.createContext()创建一个 StateContext 来传递状态,

Container:状态管理类,内部使用 state 存储状态,通过 setState 实现状态的更新,api 设计与 React 的组件基本一致。
Provider:返回 Provider,用来包裹顶层组件,向应用中注入状态管理实例,可做数据的初始化。
Subscribe:本质上是 Consumer,获取状态管理实例,在 Container 实例更新状态的时候强制更新视图。

简单的例子
我们拿最通用的计数器的例子来看 unstated 如何使用,先明确一下结构:Parent 作为父组件包含两个子组件:Child1 和 Child2。Child1 展示数字,Child2 操作数字的加减。然后,Parent 组件的外层会包裹一个根组件。
维护状态
首先,共享状态需要有个状态管理的地方,与 Redux 的 Reducer 不同的是,Unstated 是通过一个继承自 Container 实例:
import {Container} from ‘unstated’;

class CounterContainer extends Container {
constructor(initCount) {
super(…arguments);
this.state = {count: initCount || 0};
}

increment = () => {
this.setState({count: this.state.count + 1});
}

decrement = () => {
this.setState({count: this.state.count – 1});
}
}

export default CounterContainer
看上去是不是很熟悉?像一个 React 组件类。CounterContainer 继承自 Unstated 暴露出来的 Container 类,利用 state 存储数据,setState 维护状态,并且 setState 与 React 的 setState 用法一致,可传入函数。返回的是一个 promise。
共享状态
来看一下要显示数字的 Child1 组件,利用 Subscribe 与 CounterContainer 建立联系。
import React from ‘react’
import {Subscribe} from ‘unstated’
import CounterContainer from ‘./store/Counter’
class Child1 extends React.Component {
render() {
return <Subscribe to={[CounterContainer]}>
{
counter => {
return <div>{counter.state.count}</div>
}
}
</Subscribe>
}
}
export default Child1
再来看一下要控制数字加减的 Child2 组件:
import React from ‘react’
import {Button} from ‘antd’
import {Subscribe} from ‘unstated’
import CounterContainer from ‘./store/Counter’
class Child2 extends React.Component {
render() {
return <Subscribe to={[CounterContainer]}>
{
counter => {
return <div>
<button onClick={counter.increment}> 增加 </button>
<button onClick={counter.decrement}> 减少 </button>
</div>
}
}
</Subscribe>
}
}
export default Child2
Subscribe 内部返回的是 StateContext.Consumer,通过 to 这个 prop 关联到 CounterContainer 实例,使用 renderProps 模式渲染视图,Subscribe 之内调用的函数的参数就是订阅的那个状态管理实例。Child1 与 Child2 通过 Subscribe 订阅共同的状态管理实例 CounterContainer,所以 Child2 可以调用 CounterContainer 之内的 increment 和 decrement 方法来更新状态, 而 Child1 会根据更新来显示数据。
看一下父组件 Parent
import React from ‘react’
import {Provider} from ‘unstated’
import Child1 from ‘./Child1’
import Child2 from ‘./Child2’
import CounterContainer from ‘./store/Counter’

const counter = new CounterContainer(123)

class Parent extends React.Component {
render() {
return <Provider inject={[counter]}>
父组件
<Child1/>
<Child2/>
</Provider>
}
}

export default Parent
Provider 返回的是 StateContext.Provider,Parent 通过 Provider 向组件的上下文中注入状态管理实例。这里,可以不注入实例。不注入的话,Subscribe 内部就不能拿到注入的实例去初始化数据,也就是给状态一个默认值,比如上边我给的是 123。
也可以注入多个实例:
<Provider inject={[count1, count2]}>
{/*Components*}
</Provide>
那么,在 Subscribe 的时候可以拿到多个实例。
<Subscribe to={[CounterContainer1, CounterContainer2]}>
{count1, count2) => {}
</Subscribe>
分析原理
弄明白原理之前需要先明白 Unstated 提供的三个 API 之间的关系。我根据自己的理解,画了一张图:

来梳理一下整个流程:

创建状态管理类继承自 Container
生成上下文,new 一个状态管理的实例,给出默认值,注入 Provider
Subscribe 订阅状态管理类。内部通过_createInstances 方法来初始化状态管理实例并订阅该实例,具体过程如下:

从上下文中获取状态管理实例,如果获取到了,那它直接去初始化数据,如果没有获取到
那么就用 to 中传入的状态管理类来初始化实例。

将自身的更新视图的函数 onUpdate 通过订阅到状态管理实例,来实现实例内部 setState 的时候,调用 onUpdate 更新视图。
_createInstances 方法返回创建的状态管理实例,作为参数传递给 renderProps 调用的函数,函数拿到实例,操作或显示数据。

Container
用来实现一个状态管理类。可以理解为 redux 中 action 和 reducer 的结合。概念相似,但实现不同。来看一下 Container 的源码
export class Container {
constructor() {
CONTAINER_DEBUG_CALLBACKS.forEach(cb => cb(this));
this.state = null;
this.listeners = [];
}

setState(updater, callback) {
return Promise.resolve().then(() => {
let nextState = null;
if (typeof updater === ‘function’) {
nextState = updater(this.state);
} else {
nextState = updater;
}

if (nextState === null) {
callback && callback();
}
// 返回一个新的 state
this.state = Object.assign({}, this.state, nextState);
// 执行 listener,也就是 Subscribe 的 onUpdate 函数,用来强制刷新视图
const promises = this.listeners.map(listener => listener());

return Promise.all(promises).then(() => {
if (callback) {
return callback();
}
});
});
}

subscribe(fn) {
this.listeners.push(fn);
}

unsubscribe(fn) {
this.listeners = this.listeners.filter(f => f !== fn);
}
}
Container 包含了 state、listeners,以及 setState、subscribe、unsubscribe 这三个方法。

state 来存放数据,listeners 是一个数组,存放更新视图的函数。
subscribe 会将更新的函数 (Subscribe 组件内的 onUpdate) 放入 linsteners。
setState 和 react 的 setState 相似。执行时,会根据变动返回一个新的 state,

同时循环 listeners 调用其中的更新函数。达到更新页面的效果。
unsubscribe 用来取消订阅。
Provider
Provider 本质上返回的是 StateContext.Provider。
export function Provider(ProviderProps) {
return (
<StateContext.Consumer>
{parentMap => {
let childMap = new Map(parentMap);

if (props.inject) {
props.inject.forEach(instance => {
childMap.set(instance.constructor, instance);
});
}

return (
<StateContext.Provider value={childMap}>
{props.children}
</StateContext.Provider>
);
}}
</StateContext.Consumer>
);
}
它自己接收一个 inject 属性,经过处理后,将它作为 context 的值传入到上下文环境中。可以看出,传入的值为一个 map,使用 Container 类作为键,Container 类的实例作为值。Subscribe 会接收这个 map,优先使用它来实例化 Container 类,初始化数据。
可能有人注意到了 Provider 不是直接返回的 StateContext.Provider,而是套了一层 StateContext.Consumer。这样做的目的是 Provider 之内还可以嵌套 Provider。内层 Provider 的 value 可以继承自外层。
Subscribe
简单来说就是连接组件与状态管理类的一座桥梁,可以想象成 react-redux 中 connect 的作用
class Subscribe extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.instances = [];
this.unmounted = false;
}

componentWillUnmount() {
this.unmounted = true;
this.unsubscribe();
}

unsubscribe() {
this.instances.forEach((container) => {
container.unsubscribe(this.onUpdate);
});
}

onUpdate = () => new Promise((resolve) => {
if (!this.unmounted) {
this.setState(DUMMY_STATE, resolve);
} else {
resolve();
}
})

_createInstances(map, containers) {
this.unsubscribe();

if (map === null) {
throw new Error(‘You must wrap your <Subscribe> components with a <Provider>’);
}

const safeMap = map;
const instances = containers.map((ContainerItem) => {
let instance;

if (
typeof ContainerItem === ‘object’ &&
ContainerItem instanceof Container
) {
instance = ContainerItem;
} else {
instance = safeMap.get(ContainerItem);

if (!instance) {
instance = new ContainerItem();
safeMap.set(ContainerItem, instance);
}
}

instance.unsubscribe(this.onUpdate);
instance.subscribe(this.onUpdate);

return instance;
});

this.instances = instances;
return instances;
}

render() {
return (
<StateContext.Consumer>
{
map => this.props.children.apply(
null,
this._createInstances(map, this.props.to),
)
}
</StateContext.Consumer>
);
}
}
这里比较重要的是_createInstances 与 onUpdate 两个方法。StateContext.Consumer 接收 Provider 传递过来的 map,与 props 接收的 to 一并传给_createInstances。
onUpdate: 没有做什么其他事情,只是利用 setState 更新视图,返回一个 promise。它存在的意义是在订阅的时候,作为参数传入 Container 类的 subscribe,扩充 Container 类的 listeners 数组,随后在 Container 类 setState 改变状态以后,循环 listeners 的每一项就是这个 onUpdate 方法,它执行,就会更新视图。
_createInstances: map 为 provider 中 inject 的状态管理实例数据。如果 inject 了,那么就用 map 来实例化数据,否则用 this.props.to 的状态管理类来实例化。之后调用 instance.subscribe 方法(也就是 Container 中的 subscribe),传入自身的 onUpdate,实现订阅。它存在的意义是实例化 Container 类并将自身的 onUpdate 订阅到 Container 类实例,最终返回这个 Container 类的实例,作为 this.props.children 的参数并进行调用,所以在组件内部可以进行类似这样的操作:
<Subscribe to={[CounterContainer]}>
{
counter => {
return <div>
<Button onClick={counter.increment}> 增加 </Button>
<Button onClick={counter.decrement}> 减少 </Button>
</div>
}
}
</Subscribe>
总结
Unstated 上手很容易,理解源码也不难。重点在于理解发布(Container 类),Subscribe 组件实现订阅的思路。其 API 的设计贴合 React 的设计理念。也就是想要改变 UI 必须 setState。另外可以不用像 Redux 一样写很多样板代码。
理解源码的过程中受到了下面两篇文章的启发,衷心感谢:
纯粹极简的 react 状态管理组件 unstated
Unstated 浅析

退出移动版