在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之間的關係。我根據自己的理解,畫了一張圖:
[外鏈圖片轉存失敗(img-x4WUkF6I-1567334313760)(http://pre0ovlap.bkt.clouddn.com/unstated.jpg)]
來梳理一下整個流程:
- 創建狀態管理類繼承自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一樣寫很多樣板代碼。
理解源碼的過程中受到了下面兩篇文章的啓發,衷心感謝:
歡迎關注我的公衆號: 一口一個前端,不定期分享我所理解的前端知識