Unstated浅析

2,562 阅读4分钟

现状

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>
    );
  }
}