Unstated淺析

supot發表於2019-01-23

現狀

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>
    );
  }
}複製程式碼