精讀《unstated 與 unstated-next 源碼》

1 引言

unstated 是基於 Class Component 的數據流管理庫,unstated-next 是針對 Function Component 的升級版,且特別優化了對 Hooks 的支持。前端

與類 redux 庫相比,這個庫設計的別出心裁,並且這兩個庫源碼行數都特別少,與 180 行的 unstated 相比,unstated-next 只有不到 40 行,但想象空間卻更大,且用法符合直覺,因此本週精讀就會從用法與源碼兩個角度分析這兩個庫。react

2 概述

首先問,什麼是數據流?React 自己就提供了數據流,那就是 setStateuseState,數據流框架存在的意義是解決跨組件數據共享與業務模型封裝。git

還有一種說法是,React 早期聲稱本身是 UI 框架,不關心數據,所以須要生態提供數據流插件彌補這個能力。但其實 React 提供的 createContextuseContext 已經能解決這個問題,只是使用起來稍顯麻煩,而 unstated 系列就是爲了解決這個問題。github

unstated

unstated 解決的是 Class Component 場景下組件數據共享的問題。redux

相比直接拋出用法,筆者還原一下做者的思考過程:利用原生 createContext 實現數據流須要兩個 UI 組件,且實現方式冗長:api

const Amount = React.createContext(1);

class Counter extends React.Component {
  state = { count: 0 };
  increment = amount => {
    this.setState({ count: this.state.count + amount });
  };
  decrement = amount => {
    this.setState({ count: this.state.count - amount });
  };
  render() {
    return (
      <Amount.Consumer>
        {amount => (
          <div>
            <span>{this.state.count}</span>
            <button onClick={() => this.decrement(amount)}>-</button>
            <button onClick={() => this.increment(amount)}>+</button>
          </div>
        )}
      </Amount.Consumer>
    );
  }
}

class AmountAdjuster extends React.Component {
  state = { amount: 0 };
  handleChange = event => {
    this.setState({
      amount: parseInt(event.currentTarget.value, 10)
    });
  };
  render() {
    return (
      <Amount.Provider value={this.state.amount}>
        <div>
          {this.props.children}
          <input
            type="number"
            value={this.state.amount}
            onChange={this.handleChange}
          />
        </div>
      </Amount.Provider>
    );
  }
}

render(
  <AmountAdjuster>
    <Counter />
  </AmountAdjuster>
);
複製代碼

而咱們要作的,是將 setState 從具體的某個 UI 組件上剝離,造成一個數據對象實體,能夠被注入到任何組件。promise

這就是 unstated 的使用方式:微信

import React from "react";
import { render } from "react-dom";
import { Provider, Subscribe, Container } from "unstated";

class CounterContainer extends Container {
  state = {
    count: 0
  };

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

  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
}

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")
);
複製代碼

首先要爲 Provider 正名:Provider 是解決單例 Store 的最佳方案,當項目與組件都是用了數據流,須要分離做用域時,Provider 便派上了用場。若是項目僅需單 Store 數據流,那麼與根節點放一個 Provider 等價。app

其次 CounterContainer 成爲一個真正數據處理類,只負責存儲與操做數據,經過 <Subscribe to={[CounterContainer]}> RenderProps 方法將 counter 注入到 Render 函數中。框架

unstated 方案本質上利用了 setState,但將 setState 與 UI 剝離,並能夠很方便的注入到任何組件中。

相似的是,其升級版 unstated-next 本質上利用了 useState,利用了自定義 Hooks 能夠與 UI 分離的特性,加上 useContext 的便捷性,利用不到 40 行代碼實現了比 unstated 更強大的功能。

unstated-next

unstated-next 用 40 行代碼號稱 React 數據管理庫的終結版,讓咱們看看它是怎麼作到的!

仍是從思考過程提及,筆者發現其 README 也提供了對應思考過程,就以其 README 裏的代碼做爲案例。

首先,使用 Function Component 的你會這樣使用數據流:

function CounterDisplay() {
  let [count, setCount] = useState(0);
  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);
  return (
    <div> <button onClick={decrement}>-</button> <p>You clicked {count} times</p> <button onClick={increment}>+</button> </div>
  );
}
複製代碼

若是想將數據與 UI 分離,利用 Custom Hooks 就能夠完成,這不須要藉助任何框架:

function useCounter() {
  let [count, setCount] = useState(0);
  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);
  return { count, decrement, increment };
}

function CounterDisplay() {
  let counter = useCounter();
  return (
    <div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
  );
}
複製代碼

若是想將這個數據分享給其餘組件,利用 useContext 就能夠完成,這不須要藉助任何框架:

function useCounter() {
  let [count, setCount] = useState(0);
  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);
  return { count, decrement, increment };
}

let Counter = createContext(null);

function CounterDisplay() {
  let counter = useContext(Counter);
  return (
    <div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
  );
}

function App() {
  let counter = useCounter();
  return (
    <Counter.Provider value={counter}> <CounterDisplay /> <CounterDisplay /> </Counter.Provider> ); } 複製代碼

但這樣仍是顯示使用了 useContext 的 API,而且對 Provider 的封裝沒有造成固定模式,這就是 usestated-next 要解決的問題。

因此這就是 unstated-next 的使用方式:

import { createContainer } from "unstated-next";

function useCounter() {
  let [count, setCount] = useState(0);
  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);
  return { count, decrement, increment };
}

let Counter = createContainer(useCounter);

function CounterDisplay() {
  let counter = Counter.useContainer();
  return (
    <div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
  );
}

function App() {
  return (
    <Counter.Provider> <CounterDisplay /> <CounterDisplay /> </Counter.Provider> ); } 複製代碼

能夠看到,createContainer 能夠將任何 Hooks 包裝成一個數據對象,這個對象有 ProvideruseContainer 兩個 API,其中 Provider 用於對某個做用域注入數據,而 useContainer 能夠取到這個數據對象在當前做用域的實例。

對 Hooks 的參數也進行了規範化,咱們能夠經過 initialState 設定初始化數據,且不一樣做用域能夠嵌套並賦予不一樣的初始化值:

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState);
  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);
  return { count, decrement, increment };
}

const Counter = createContainer(useCounter);

function CounterDisplay() {
  let counter = Counter.useContainer();
  return (
    <div> <button onClick={counter.decrement}>-</button> <span>{counter.count}</span> <button onClick={counter.increment}>+</button> </div>
  );
}

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
      <Counter.Provider initialState={2}>
        <div>
          <div>
            <CounterDisplay />
          </div>
        </div>
      </Counter.Provider>
    </Counter.Provider>
  );
}
複製代碼

能夠看到,React Hooks 已經很是適合作狀態管理,而生態應該作的事情是儘量利用其能力進行模式化封裝。

有人可能會問,取數和反作用怎麼辦?redux-saga 和其餘中間件都沒有,這個數據流是否是閹割版?

首先咱們看 Redux 爲何須要處理反作用的中間件。這是由於 reducer 是一個同步純函數,其返回值就是操做結果中間不能有異步,且不能有反作用,因此咱們須要一種異步調用 dispatch 的方法,或者一個反作用函數來存放這些 「髒」 邏輯。

而在 Hooks 中,咱們能夠隨時調用 useState 提供的 setter 函數修改值,這早已自然解決了 reducer 沒法異步的問題,同時也實現了 redux-chunk 的功能。

而異步功能也被 useEffect 這個 React 官方 Hook 替代。咱們看到這個方案能夠利用 React 官方提供的能力徹底覆蓋 Redux 中間件的能力,對 Redux 庫實現了降維打擊,因此下一代數據流方案隨着 Hooks 的實現是真的存在的

最後,相比 Redux 自身以及其生態庫的理解成本(筆者不才,初學 Redux 以及其周邊 middleware 時理解了很久),Hooks 的理解學習成本明顯更小。

不少時候,人們排斥一個新技術,並非由於新技術很差,而是這可能讓本身多年精通的老手藝帶來的 「競爭優點」 徹底消失。可能一個織布老專家手工織布效率是入門學員的 5 倍,但換上織布機器後,這個差別很快會被抹平,老織布專家面臨被淘汰的危機,因此維護這份老手藝就是維護他本身的利益。但願每一個團隊中的老織布工人都能主動引入織布機。

再看取數中間件,咱們通常須要解決 取數業務邏輯封裝取數狀態封裝,經過 redux 中間件能夠封裝在內,經過一個 dispatch 解決。

其實 Hooks 思惟下,利用 swr useSWR 同樣能解決:

function Profile() {
  const { data, error } = useSWR("/api/user");
}
複製代碼

取數的業務邏輯封裝在 fetcher 中,這個在 SWRConfigContext.Provider 時就已注入,還能夠控制做用域!徹底利用 React 提供的 Context 能力,能夠感覺到實現底層原理的一致性和簡潔性,越簡單越優美的數學公式越多是真理。

而取數狀態已經封裝在 useSWR 中,配合 Suspense 能力,連 Loading 狀態都不用關心了。

3 精讀

unstated

咱們再梳理一下 unstated 這個庫作了哪些事情。

  1. 利用 Provider 申明做用範圍。
  2. 提供 Container 做爲能夠被繼承的類,繼承它的 Class 做爲 Store。
  3. 提供 Subscribe 做爲 RenderProps 用法注入 Store,注入的 Store 實例由參數 to 接收到的 Class 實例決定。

對於第一點,Provider 在 Class Component 環境下要初始化 StateContext,這樣才能在 Subscribe 中使用:

const StateContext = createReactContext(null);

export function Provider(props) {
  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>
  );
}
複製代碼

對於第二點,對於 Container,須要提供給 Store setState API,按照 React 的 setState 結構實現了一遍。

值得注意的是,還存儲了一個 _listeners 對象,而且可經過 subscribeunsubscribe 增刪。

_listeners 存儲的實際上是當前綁定的組件 onUpdate 生命週期,而後在 setState 時主動觸發對應組件的渲染。onUpdate 生命週期由 Subscribe 函數提供,最終調用的是 this.setState,這個在 Subscribe 部分再說明。

如下是 Container 的代碼實現:

export class Container<State: {}> {
  state: State;
  _listeners: Array<Listener> = [];

  constructor() {
    CONTAINER_DEBUG_CALLBACKS.forEach(cb => cb(this));
  }

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

      if (typeof updater === "function") {
        nextState = updater(this.state);
      } else {
        nextState = updater;
      }

      if (nextState == null) {
        if (callback) callback();
        return;
      }

      this.state = Object.assign({}, this.state, nextState);

      let promises = this._listeners.map(listener => listener());

      return Promise.all(promises).then(() => {
        if (callback) {
          return callback();
        }
      });
    });
  }

  subscribe(fn: Listener) {
    this._listeners.push(fn);
  }

  unsubscribe(fn: Listener) {
    this._listeners = this._listeners.filter(f => f !== fn);
  }
}
複製代碼

對於第三點,Subscriberender 函數將 this.props.children 做爲一個函數執行,並把對應的 Store 實例做爲參數傳遞,這經過 _createInstances 函數實現。

_createInstances 利用 instanceof 經過 Class 類找到對應的實例,並經過 subscribe 將本身組件的 onUpdate 函數傳遞給對應 Store 的 _listeners,在解除綁定時調用 unsubscribe 解綁,防止沒必要要的 renrender。

如下是 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.unsubscribe(this.onUpdate);
    });
  }

  onUpdate: Listener = () => {
    return new Promise(resolve => {
      if (!this.unmounted) {
        this.setState(DUMMY_STATE, resolve);
      } else {
        resolve();
      }
    });
  };

  _createInstances(
    map: ContainerMapType | null,
    containers: ContainersType
  ): Array<ContainerType> {
    this._unsubscribe();

    if (map === null) {
      throw new Error(
        "You must wrap your <Subscribe> components with a <Provider>"
      );
    }

    let safeMap = map;
    let 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 將 State 外置是經過自定義 Listener 實現的,在 Store setState 時觸發收集好的 Subscribe 組件的 rerender。

unstated-next

unstated-next 這個庫只作了一件事情:

  1. 提供 createContainer 將自定義 Hooks 封裝爲一個數據對象,提供 Provider 注入與 useContainer 獲取 Store 這兩個方法。

正如以前解析所說,unstated-next 可謂將 Hooks 用到了極致,認爲 Hooks 已經徹底具有數據流管理的所有能力,咱們只要包裝一層規範便可:

export function createContainer(useHook) {
  let Context = React.createContext(null);

  function Provider(props) {
    let value = useHook(props.initialState);
    return <Context.Provider value={value}>{props.children}</Context.Provider>;
  }

  function useContainer() {
    let value = React.useContext(Context);
    if (value === null) {
      throw new Error("Component must be wrapped with <Container.Provider>");
    }
    return value;
  }

  return { Provider, useContainer };
}
複製代碼

可見,Provider 就是對 value 進行了約束,固化了 Hooks 返回的 value 直接做爲 value 傳遞給 Context.Provider 這個規範。

useContainer 就是對 React.useContext(Context) 的封裝。

真的沒有其餘邏輯了。

惟一須要思考的是,在自定義 Hooks 中,咱們用 useState 管理數據仍是 useReducer 管理數據的問題,這個是個仁者見仁的問題。不過咱們能夠對自定義 Hooks 進行嵌套封裝,支持一些更復雜的數據場景,好比:

function useCounter(initialState = 0) {
  const [count, setCount] = useState(initialState);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  return { count, decrement, increment };
}

function useUser(initialState = {}) {
  const [name, setName] = useState(initialState.name);
  const [age, setAge] = useState(initialState.age);
  const registerUser = userInfo => {
    setName(userInfo.name);
    setAge(userInfo.age);
  };
  return { user: { name, age }, registerUser };
}

function useApp(initialState) {
  const { count, decrement, increment } = useCounter(initialState.count);
  const { user, registerUser } = useUser(initialState.user);
  return { count, decrement, increment, user, registerUser };
}

const App = createContainer(useApp);
複製代碼

4 總結

借用 unstated-next 的標語:「never think about React state management libraries ever again」 - 用了 unstated-next 不再要考慮其餘 React 狀態管理庫了。

而有意思的是,unstated-next 自己也只是對 Hooks 的一種模式化封裝,Hooks 已經能很好解決狀態管理的問題,咱們真的不須要 「再造」 React 數據流工具了。

討論地址是:精讀《unstated 與 unstated-next 源碼》 · Issue #218 · dt-fe/weekly

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章
相關標籤/搜索