理解 React 輕量狀態管理庫 Unstated

在React寫應用的時候,不免遇到跨組件通訊的問題。如今已經有不少的解決方案。react

  • React自己的Context
  • Redux結合React-redux
  • Mobx結合mobx-react

React 的新的Context api本質上並非React或者Mbox這種狀態管理工具的替代品,充其量只是對React 自身狀態管理短板的補充。而Redux和Mbox這兩個庫自己並非爲React設計的,對於一些小型的React應用 比較重。redux

基本概念

Unstated是基於context API。也就是使用React.createContext()建立一個StateContext來傳遞狀態,segmentfault

  • Container:狀態管理類,內部使用state存儲狀態,經過setState實現狀態的更新,api設計與React的組件基本一致。
  • Provider:返回Provider,用來包裹頂層組件,嚮應用中注入狀態管理實例,可作數據的初始化。
  • Subscribe:本質上是Consumer,獲取狀態管理實例,在Container實例更新狀態的時候強制更新視圖。

簡單的例子

咱們拿最通用的計數器的例子來看unstated如何使用,先明確一下結構:Parent做爲父組件包含兩個子組件:Child1和Child2。 Child1展現數字,Child2操做數字的加減。而後,Parent組件的外層會包裹一個根組件。api

維護狀態

首先,共享狀態須要有個狀態管理的地方,與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。promise

共享狀態

來看一下要顯示數字的Child1組件,利用Subscribe與CounterContainer創建聯繫。bash

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組件:antd

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以內調用的函數的參數就是訂閱的那個狀態管理實例。 Child1Child2經過Subscribe訂閱共同的狀態管理實例CounterContainer,因此Child2能夠調用 CounterContainer以內的increment和decrement方法來更新狀態,而Child1會根據更新來顯示數據。app

看一下父組件Parentide

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之間的關係。我根據本身的理解,畫了一張圖:

來梳理一下整個流程:

  1. 建立狀態管理類繼承自Container
  2. 生成上下文,new一個狀態管理的實例,給出默認值,注入Provider
  3. 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淺析

相關文章
相關標籤/搜索