[源碼閱讀]純粹極簡的react狀態管理組件unstated

簡介

unstated是一個極簡的狀態管理組件react

看它的簡介:State so simple, it goes without saying

對比

對比redux:

  • 更加靈活(相對的缺點是缺乏規則,須要使用者的自覺)

    redux的狀態是存放在一棵樹內,採用嚴格的單向流git

    unstated的狀態是用戶本身定義,說白了就是object,能夠放在一個組件的內,也能夠放在多個組件內github

  • 針對React,一致的API

    redux必須編寫reduceraction,經過dispatch(action)改變狀態,它不限框架redux

    unstated改變狀態的API徹底與React一致,使用this.setState,固然和ReactsetState不一樣,
    可是它的底層也是用到了setState去更新視圖segmentfault

  • 功能相對簡單

    unstated沒有中間件功能,每次狀態改變(不論是否相等),都會從新渲染(V2.1.1)數組

    能夠自定義listener,每次更新狀態時都會執行。promise

對比React的自帶state:

  • 天生將組件分割爲Container(狀態管理)Component(視圖管理)
  • 靈活配置共享狀態或者私有狀態
  • 支持promise
快速瞭解請直接跳到 總結

初識

3大板塊和幾個關鍵變量app

Provider: 注入狀態實例,傳遞map,本質是Context.Provider,可嵌套達成鏈式傳遞
Container: 狀態管理類,遵循React的API,發佈訂閱模式,經過new生成狀態管理實例
Subscribe: 訂閱狀態組件,本質是Context.Consumer,接收Provider提供的map,視圖渲染組件
map: new Map(),經過類查找當前類建立的狀態管理實例

深刻

這裏引入官方例子框架

// @flow
import React from 'react';
import { render } from 'react-dom';
import { Provider, Subscribe, Container } from 'unstated';

type CounterState = {
  count: number
};
// 定義一個狀態管理類
class CounterContainer extends Container<CounterState> {
  state = {
    count: 0
  };

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

  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
}
// 渲染視圖組件(Context.Consumer的模式)
function Counter() {
  return (
    <Subscribe to={[CounterContainer]}>
      {counter => (
        <div>
          <button onClick={() => counter.decrement()}>-</button>
          <span>{counter.state.count}</span>
          <button onClick={() => counter.increment()}>+</button>
        </div>
      )}
    </Subscribe>
  );
}

render(
  <Provider>
    <Counter />
  </Provider>,
  document.getElementById('root')
);

這裏Counter是咱們自定義的視圖組件,首先使用<Provider>包裹,接着在Counter內部,調用<Subscribe>組件,
傳遞一個數組給props.to,這個數組內存放了Counter組件須要使用的狀態管理類(此處也可傳遞狀態管理實例)。dom

Provider

export function Provider(props: ProviderProps) {
  return (
    <StateContext.Consumer>
      {parentMap => {
        let childMap = new Map(parentMap);
        // 外部注入的狀態管理實例
        if (props.inject) {
          props.inject.forEach(instance => {
            childMap.set(instance.constructor, instance);
          });
        }

        // 負責將childMap傳遞,初始爲null
        return (
          <StateContext.Provider value={childMap}>
            {props.children}
          </StateContext.Provider>
        );
      }}
    </StateContext.Consumer>
  );
}

這裏的模式是

<Consumer>
  ()=>{
    /* ... */
    return <Provider>{props.children}<Provider />
  }
</Consumer>

有3個注意點:

  1. 外層嵌套<Consumer>能夠嵌套調用。

    <Provider value={...}>
     /* ... */
     <Provider value={此處繼承了上面的value}>
     /* ... */ 
    </Provider>
  2. props.inject能夠注入現成的狀態管理實例,添加到map之中。
  3. 返回值寫成props.children

返回值寫成props.children的意義

簡單一句話歸納,這麼寫能夠避免React.Context改變致使子組件的重複渲染。

具體看這裏:避免React Context致使的重複渲染

Container

export class Container<State: {}> {
  // 保存狀態 默認爲{}
  state: State;
  // 保存監聽函數,默認爲[]
  _listeners: Array<Listener> = [];

  setState(
    updater: $Shape<State> | ((prevState: $Shape<State>) => $Shape<State>),
    callback?: () => void
  ): Promise<void> {
    return Promise.resolve().then(() => {
      let nextState;

      /* 利用Object.assign改變state */

      // 執行listener(promise)
      let promises = this._listeners.map(listener => listener());

      // 全部Promise執行完畢
      return Promise.all(promises).then(() => {
        // 所有listener執行完畢,執行回調
        if (callback) {
          return callback();
        }
      });
    });
  }

  // 增長訂閱(這裏默認的訂閱就是React的setState空值(爲了從新渲染),也能夠添加自定義監聽函數)
  subscribe(fn: Listener) {
    this._listeners.push(fn);
  }

  // 取消訂閱
  unsubscribe(fn: Listener) {
    this._listeners = this._listeners.filter(f => f !== fn);
  }
}

Container內部邏輯很簡單,改變state,執行監聽函數。

其中有一個_listeners,是用於存放監聽函數的。

每一個狀態管理實例存在一個默認監聽函數onUpdate
這個默認的監聽函數的做用就是調用React的setState強制視圖從新渲染

這裏的監聽函數內部返回Promise,最後經過Promise.all確保執行完畢,而後執行回調參數

所以setState在外面使用也可使用then

例如,在官方例子中:

increment() {
    this.setState({ count: this.state.count + 1 },()=>console.log('2'))
    .then(()=>console.log('3') )
    console.log('1') 
  }
  // 執行順序是 1 -> 2 ->3

2個注意點:

  1. setStateReact API一致,第一個參數傳入object或者function,第二個傳入回調
  2. 這裏經過Promise.resolve().then模擬this.setState的異步執行

關於Promise.resolve和setTimeout的區別

簡單的說二者都是異步調用,Promise更快執行。

  • setTimeout(()=>{},0)會放入下一個新的任務隊列
  • Promise.resolve().then({})會放入微任務,在調用棧爲空時馬上補充調用棧並執行(簡單理解爲當前任務隊列尾部)

更多詳細能夠看這裏提供的2個視頻:https://stackoverflow.com/a/38752743

Subscribe

export class Subscribe<Containers: ContainersType> extends React.Component<
  SubscribeProps<Containers>,
  SubscribeState
> {
  state = {};
  // 存放傳入的狀態組件
  instances: Array<ContainerType> = [];
  unmounted = false;

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

  _unsubscribe() {
    this.instances.forEach(container => {
      // container爲當前組件的每個狀態管理實例
      // 刪除listeners中的this.onUpdate
      container.unsubscribe(this.onUpdate);
    });
  }

  onUpdate: Listener = () => {
    return new Promise(resolve => {
      // 組件未被卸載
      if (!this.unmounted) {
        // 純粹是爲了讓React更新組件
        this.setState(DUMMY_STATE, resolve);
      } else {
        // 已經被卸載則直接返回
        resolve();
      }
    });
  };
  
  /* ... */
}

這裏的關鍵就是instances,用於存放當前組件的狀態管理實例

當組件unmount的時候,會unsubscribe當前狀態管理實例的默認監聽函數,那麼若是當前的狀態管理實例是共享的,會不會有影響呢?

不會的。日後看能夠知道,當state每次更新,都會從新建立新的狀態管理實例(由於props.to的值可能會發生變化,例如取消某一個狀態管理實例),
而每次建立時,都會先unsubscribesubscribe,確保不會重複添加監聽函數。

onUpdate就是建立狀態管理組件時默認傳遞的監聽函數,用的是ReactsetState更新一個DUMMY_STATE(空對象{})。

export class Subscribe<Containers: ContainersType> extends React.Component<
  SubscribeProps<Containers>,
  SubscribeState
> {
  /* 上面已講 */

  _createInstances(
    map: ContainerMapType | null,
    containers: ContainersType
  ): Array<ContainerType> {
    // 首先所有instances解除訂閱
    this._unsubscribe();

    // 必須存在map 必須被Provider包裹纔會有map
    if (map === null) {
      throw new Error(
        'You must wrap your <Subscribe> components with a <Provider>'
      );
    }

    let safeMap = map;
    // 從新定義當前組件的狀態管理組件(根據to傳入的數組)
    let instances = containers.map(ContainerItem => {
      let instance;

      // 傳入的是Container組件,則使用
      if (
        typeof ContainerItem === 'object' &&
        ContainerItem instanceof Container
      ) {
        instance = ContainerItem;
      } else {
        // 傳入的不是Container,多是其餘自定義組件等等(須要用new執行),嘗試獲取
        instance = safeMap.get(ContainerItem);

        // 不存在則以它爲key,value是新的Container組件
        if (!instance) {
          instance = new ContainerItem();
          safeMap.set(ContainerItem, instance);
        }
      }

      // 先解綁再綁定,避免重複訂閱
      instance.unsubscribe(this.onUpdate);
      instance.subscribe(this.onUpdate);

      return instance;
    });

    this.instances = instances;
    return instances;
  }
  
  /* ... */
}

_createInstances內部,若是檢查到傳入的props.to的值已是狀態管理實例(私有狀態組件),那麼直接使用便可,
若是傳入的是類class(共享狀態組件),會嘗試經過查詢map,不存在的則經過new建立。

export class Subscribe<Containers: ContainersType> extends React.Component<
  SubscribeProps<Containers>,
  SubscribeState
> {
  
  /* 上面已講 */
  
  render() {
    return (
      <StateContext.Consumer>
      /* Provider傳遞的map */
      {map =>
          // children是函數
          this.props.children.apply(
            null,
            // 傳給子函數的參數(傳進當前組件的狀態管理實例)
            this._createInstances(map, this.props.to)
          )
        }
      </StateContext.Consumer>
    );
  }
}

每一次render都會建立新的狀態管理實例

到此,3大板塊已經閱讀完畢。

總結

  1. 簡單易用,與React一致的API,一致的書寫模式,讓使用者很快上手。
  2. 並無規定如何管理這些狀態管理類,很是靈活。

    咱們能夠學redux將全部狀態放到一個共享狀態管理實例內部,
    例如經過Providerinject屬性注入,

    或者針對每個組件建立單獨的狀態管理實例(可共享可獨立)(unstated做者推薦),

    一切能夠按照本身的想法,但同時也要求使用者本身定義一些規則去約束寫法。

  3. 僅僅是管理了狀態,每次更新都是一個全新的instance集合,並無作任何對比,須要咱們在視圖層本身實現。
  4. 返回值寫成props.children意義
  5. 關於Promise.resolve().then({})setTimeout(()=>{},0)區別

導圖

unstated.png


源碼閱讀專欄對一些中小型熱門項目進行源碼閱讀和分析,對其總體作出導圖,以便快速瞭解內部關係及執行順序。
當前源碼(帶註釋),以及更多源碼閱讀內容: https://github.com/stonehank/sourcecode-analysis,歡迎 fork,求
相關文章
相關標籤/搜索