現狀
react狀態管理的主流方案一般是Redux和Mobx。
Redux是函式式的解決方案,需要寫大量的樣板檔案,可以使用Dva或者Rematch來簡化開發。
Mobx是通過proxy和defineProperty來劫持資料,對每個資料變動進行響應。
在專案裡使用Redux和Mobx,都需要配合一些其他的功能庫,比如react-redux。
如果只是想進行簡單的元件通訊,去共享一些資料,Redux和Mobx可能會比較重。如果直接使用context,可能要自己封裝一些元件。
Unstated
Unstated是一個輕量級的狀態管理工具,並且在api設計的時候沿用react的設計思想,能夠快速的理解和上手。如果只是為了解決元件的通訊問題,可以嘗試使用它,不需要依賴其他的第三方庫。
一個簡單的栗子
import React, { Component } from 'react';
import { Button, Input } from 'antd';
import { Provider, Subscribe, Container } from 'unstated';
class CounterContainer extends Container {
constructor(initCount) {
super();
this.state = { count: initCount || 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
decrement = () => {
this.setState({ count: this.state.count - 1 });
};
}
const Counter = () => (
<Subscribe to={[CounterContainer]}>
{
counter => (
<div>
<span>{counter.state.count}</span>
<Button onClick={counter.decrement}>-</Button>
<Button onClick={counter.increment}>+</Button>
</div>
)
}
</Subscribe>
);
export default class CounterProvider extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<Provider>
<Counter />
</Provider>
);
}
}
複製程式碼
淺析
Unstated丟擲三個物件,分別是Container、Subscribe和Provider。
Unstated會使用React.createContext來建立一個StateContext物件,用來進行狀態的傳遞。
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();
}
this.state = Object.assign({}, this.state, nextState);
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類包含了setState、subscribe和unsubscribe三個方法和state、listeners兩個屬性。
state用來儲存資料,listeners用來存放訂閱的方法。
subscribe和unsubscribe分別用來訂閱方法和解除訂閱,當setState方法被呼叫時,會去觸發listeners中所有的訂閱方法。從程式碼中可以看出,subscribe訂閱的方法要返回一個promise。
setState用來更新state。這個方法和react中的setState類似,可以接收一個物件或者方法,最後會使用Object.assign對state進行合併生成一個新的state。
setState()注意點
不要在設定state後立即讀取state
class CounterContainer extends Container {
state = { count: 0 };
increment = () => {
this.setState({ count: 1 });
console.log(this.state.count); // 0
};
}
複製程式碼
如果需要上一個state來計算下一個state,請傳入函式
class CounterContainer extends Container {
state = { count: 0 };
increment = () => {
this.setState(state => {
return { count: state.count + 1 };
});
};
}
複製程式碼
Unstated的setState返回一個promise,可以使用await
class CounterContainer extends Container {
state = { count: 0 };
increment = async () => {
await this.setState({ count: 1 });
console.log(this.state.count); // 1
};
}
複製程式碼
Subscribe
// 簡單處理過的程式碼
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>
);
}
}
複製程式碼
Unstated的Subscribe是一個react Component,且返回的是StateContext的Consumer。
兩個關鍵的方法是createInstances和onUpdate。
onUpdate
這個方法用來被Container物件進行訂閱,呼叫這個方法會觸發Subscribe的setState,進而重新渲染Subscribe元件。
createInstances
createInstances接收兩個引數,map是StateContext.Provider傳過來的值,第二個引數是元件接收的to這個prop。
對props傳進來的Container類進行處理。如果safeMap沒有這個Container的例項化物件,那麼先例項化一個instance,然後將這個Container增加到safeMap中,Container作為鍵,instance作為值;如果傳進來的safeMap已經有這個Container類的例項,那麼直接賦值給instance。Container的例項生成後,訂閱onUpdate方法。
Provider
// 簡單處理過的程式碼
function Provider(props) {
return (
<StateContext.Consumer>
{
(parentMap) => {
const 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>
);
}
複製程式碼
Provider接收inject這個prop,inject是一個陣列,陣列的每一項都是Container的例項。
通過上面的程式碼可以看出,context的值是一個map,這個map的鍵是Container類,值是Container類的例項。Unstated通過Provider的inject屬性,讓我們可以做一些Container類初始化的工作。在Subscribe接收的map中,如果已經存在某個Container類的鍵值對,那麼就直接使用這個例項進行處理
import React, { Component } from 'react';
import { Button, Input } from 'antd';
import { Provider, Subscribe, Container } from 'unstated';
class AppContainer extends Container {
constructor(initAmount) {
super();
this.state = {
amount: initAmount || 1,
};
}
setAmount(amount) {
this.setState({ amount });
}
}
class CounterContainer extends Container {
state = {
count: 0,
};
increment(amount) {
this.setState({ count: this.state.count + amount });
}
decrement(amount) {
this.setState({ count: this.state.count - amount });
}
}
function Counter() {
return (
<Subscribe to={[AppContainer, CounterContainer]}>
{
(app, counter) => (
<div>
<span>Count: {counter.state.count}</span>
<Button onClick={() => counter.decrement(app.state.amount)}>-</Button>
<Button onClick={() => counter.increment(app.state.amount)}>+</Button>
</div>
)
}
</Subscribe>
);
}
function App() {
return (
<Subscribe to={[AppContainer]}>
{
app => (
<div>
<Counter />
<span>Amount: </span>
<Input
type="number"
value={app.state.amount}
onChange={(event) => {
app.setAmount(parseInt(event.currentTarget.value, 10));
}}
/>
</div>
)
}
</Subscribe>
);
}
export default class CounterProvider extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const initAppContainer = new AppContainer(3);
return (
<Provider inject={[initAppContainer]}>
<App />
{/* <Counter /> */}
</Provider>
);
}
}複製程式碼