初試Mobx——讓狀態管理自動化

前言 ✍️

前端的場景愈來愈複雜,現階段,新項目都會採用VueAngularReact之一來管理數據到視圖的映射關係,它們都有本身管理組件狀態、生命週期的獨特機制,可是在複雜場景下,仍是會採用像VuexNgrxRedux這樣的狀態容器來管理重要的全局狀態前端

我工做最主要用的仍是React,在項目中,我使用Mobx來做爲React狀態管理的補充,加快編碼的效率,本文主要記錄一些Mobx的用法。react

介紹 📚

Mobx是一個狀態管理庫,在狀態依賴的描述上面有獨特的優點,就像是在寫公式同樣,它能讓開發者更簡潔的聲明描述屬性狀態的依賴關係,自動的完成相關依賴的更新、引發反作用。數組

用法 🔧

Mobx的使用很靈活,能夠將observable的特性直接做用在一個對象中,也能夠聲明在類中,甚至直接寫入React組件類的屬性中(在Mobx的視角中與類沒有區別)。性能優化

直接裝飾對象

直接使用 observable 包裝的對象,會得到Mobx給予的能力。bash

import * as mobx from "mobx";

// 聲明一個對象是 observable
const myObj = mobx.observable({
  a: 1,
  b: 3,
  get c() {
    return this.b * 2;
  }
});

// 註冊一個反作用函數
mobx.autorun(() => {
  console.log("a", myObj.a);
});
mobx.autorun(() => {
  console.log("c", myObj.c);
});
// 改變這個對象的屬性
myObj.a = false;
myObj.a = "hello";
myObj.b = 4;

/** 依次輸出 a 1 c 6 a false a hello c 8 */
複製代碼

已經能夠看出Mobx的一些特性了dom

  1. 將對象轉變爲可觀察。
  2. 對象屬性發生變化時,註冊的反作用函數自動觸發。
  3. 與反作用無關的屬性發生變化時,反作用不會觸發,上例的myObj.b,變化時不會觸發只與a有關的反作用。
  4. c這個getter屬性會被autorun反作用記錄到關於b的依賴,當b發生變化,關聯c的反作用也會被觸發。

知道以上的規則,就能夠直接在項目中嘗試它了異步

在類中聲明其屬性爲 observable

import { observable, computed, action, autorun, flow } from "mobx";

// 使用屬性裝飾器聲明
class SimpleStore {
  @observable a = 1;
  @observable c = 2;

  @computed get b() {
    return this.a * this.a + 1;
  }
  @action setA(a) {
    this.a = a;
  }
  asyncUpdate = flow(function*() {
    const next1 = yield new Promise(res => setTimeout(() => res(3), 1000));
    this.a = next1;
    const next2 = yield new Promise(res => setTimeout(() => res(4), 1000));
    this.a = next2;
  });
}

const store = new SimpleStore();

autorun(() => {
  console.log(store.a);
});

store.setA(2);
store.asyncUpdate();
/** 依次輸出 1 2 // setA(2) 3 // asyncUpdate() 1s 4 // asyncUpdate() 2s */
複製代碼

從類的observable聲明中又能看出一些東西來:async

  1. 須要使用裝飾器的語法來快速的聲明Mobx相關的功能,相比直接使用對象,須要輸入的工做量會大一些,可是能夠對其運做有更細粒度的控制
  2. 實例化的類對象也擁有了observable的能力,在字段上加上@observable,該字段就會被反作用記錄到。
  3. @action是用來聲明改變@observable字段的方法。若是開啓瞭如下配置,將強制使用@action方法來修改屬性,不然會報錯。
mobx.configure({
    enforceActions: true
});
複製代碼
  1. flowMobx提供用於修飾異步action的方法。其實就是一個async/await方法的Generator實現,最棒的特性就是這個方法返回一個 Promise,是能夠取消的。

react 組件中使用 Mobx

由於 react 組件須要監聽 observable 的變化, render 的邏輯其實就是反作用,使用 autorun 的正確方式是引入 mobx-react 庫。導入 observer 這個高階組件,來自動完成 autorun 的註冊與銷燬。ide

類組件中

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable } from "mobx";
import { observer, Observer } from "mobx-react";

@observer // 高階組件,讓react組件的render在mobx的autorun上下文中運行
class Counter extends React.Component {
  // 能夠直接使用 observable 裝飾使用,代替 react 本身的 state,更新屬性比setState要直接一些
  @observable count = 0;
  @observable unused = 0;

  handleInc = () => this.count++;
  handleDec = () => this.unused--;

  render() {
    console.log("render");
    return (
      <div>
        {this.count}
        <button onClick={this.handleInc}>+</button>
        <button onClick={this.handleDec}>-</button>
      </div>
    );
  }
}
複製代碼

上例中,對 count 的更新會強制組件更新,對 unused 的更新不會致使從新渲染,由於 render 僅僅聲明瞭對 count 的使用, render又被高階組件用 autorun 包裝過,autorun 其實有返回值,用於銷燬這個反作用,不過被 reactunmount 生命週期自動銷燬了。函數

函數組件中

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable } from "mobx";
import { observer, Observer, useLocalStore } from "mobx-react";

// 使用 observer 直接包裝函數組件
const Counter = observer(() => {
  // 使用 useLocalStore 建立一個局部的 observable
  const local = useLocalStore(() => ({
    count: 0,
    unused: 0,
    handleInc() {
      this.count++;
    },
    handleDec() {
      this.unused--;
    }
  }));
  console.log("render");
  return (
    <div>
      {local.count}
      <button onClick={() => local.handleInc()}>+</button>
      <button onClick={() => local.handleDec()}>-</button>
    </div>
  );
});
複製代碼

注意:使用 observer : 這個 autorun 的上下文僅僅用於當前 render 直接訪問的屬性,若是對 observable 屬性的訪問發生在子元素的 props 且爲函數時,須要手動使用 <Observer render={()=><JSX>...</JSX>}/> 將其放入新的 autorun 上下文中,不然更新不會生效。

外部的 observable 對象

能夠直接在外面建立 observable 對象或類,再用 observer 消費它。這裏介紹一下使用全局 Store 的方式。

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable } from "mobx";
import { observer, Observer, useLocalStore } from "mobx-react";

const store1 = observable({
  a: 1,
  b: "hello",
  incA() {
    this.a++;
  },
  repeatB() {
    this.b += this.b;
  },
  asyncIncA() {
    setTimeout(() => {
      this.a++;
    }, 1000);
  }
});

// 主要代碼
//// {
const stores = {
  store1
};

type TStore = typeof stores;
const storeCtx = React.createContext<TStore>(stores);

const StoreProvider = ({ children }) => (
  <storeCtx.Provider value={stores}>{children}</storeCtx.Provider>
);

const useSore = () => React.useContext(storeCtx);

//// }
// 主要代碼

const UsingStore = observer(() => {
  const { store1 } = useSore();

  return (
    <div>
      <div>a:{store1.a}</div>
      <div>b:{store1.b}</div>
      <button onClick={() => store1.incA()}>incA</button>
      <button onClick={() => store1.asyncIncA()}>asyncIncA</button>
      <button onClick={() => store1.repeatB()}>repeatB</button>
    </div>
  );
});

@observer
class UsingStoreInClass extends React.Component {
  static contextType = storeCtx;

  render() {
    const { store1 } = this.context as TStore;
    return (
      <div>
        <div>a:{store1.a}</div>
        <div>b:{store1.b}</div>
        <button onClick={() => store1.incA()}>incA</button>
        <button onClick={() => store1.repeatB()}>repeatB</button>
      </div>
    );
  }
}

const App = () => {
  return (
    <>
      <StoreProvider>
        <UsingStore />
        <br />
        <UsingStoreInClass />
        <br />
      </StoreProvider>
    </>
  );
};
複製代碼

主要的代碼段就是建立一個 stores 並放入 Context,以後類組件和函數組件都用 observer裝飾,從 Context 拿出這個全局狀態使用,一旦這個全局狀態有更新,相關的組件都會被通知到並從新 render

這裏仍是要多說一句,不要把應用的所有狀態放在全局 Store 裏面,這樣狀態管理的難度會大大增長,內存資源的釋放每每不到位,應該交由局部的狀態讓 react的生命週期函數來幫咱們作這些事情,尤爲是 React v16.8 提供的 Hooks就是不錯的選擇 ,應該只把一些 關鍵的全局可變狀態 放入全局 Store,好比用戶信息。

一些技巧

調試

當使用 observable 包裝一個對象或屬性時,會遞歸的將其轉換成 observable,在 console.log 查看調試的時候很不方便,充滿了 Proxy(若是用的是 Mobx 5.x),可使用 mobx.toJS 來將其轉換成一個普通的對象

優化

有些時候,遞歸將屬性轉成 observable 粒度太細了,很不必,其實也能夠減小這部分的 Proxy 開銷,方法是使用對屬性使用 observable.refobservable.shallow,或者對屬性直接用 observable.objectobservable.arrayobservable.map 建立時傳入 option {deep:false}來調節。

鏈式反應

一些複雜場景下,計算屬性每每是根據依賴異步獲取的,使用 computed 顯得不合適,可使用多個 observable 並用 reaction 來執行獲取邏輯。

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable, reaction, autorun } from "mobx";
import { observer, Observer, useLocalStore } from "mobx-react";

class ChainDemo {
  @observable a = 0;
  @observable b = 0;
  @observable c = 0;
  @observable d = 0;

  init = () => {
    const disposer = [
      reaction(
        () => {
          const val = this.a;
          return new Promise<number>(res =>
            setTimeout(() => res(val + 1), 100)
          );
        },
        async p => {
          this.b = await p;
        }
      ),
      reaction(
        () => {
          const val = this.b;
          return new Promise<number>(res =>
            setTimeout(() => res(val + 1), 100)
          );
        },
        async p => {
          this.c = await p;
        }
      ),
      reaction(
        () => {
          const val = this.c;
          return new Promise<number>(res =>
            setTimeout(() => res(val + 1), 100)
          );
        },
        async p => {
          this.d = await p;
        }
      )
    ];

    return () => disposer.forEach(d=>d());
  };
}

const chain = new ChainDemo();
chain.init();

autorun(() => {
  console.log(chain.a, chain.b, chain.c, chain.d);
});

chain.a = 2;

/**
 * 0 0 0 0
 * 2 0 0 0
 * 2 3 0 0
 * 2 3 4 0
 * 2 3 4 5
 * /
複製代碼

上例中使用了幾個延遲計算取值,狀態根據咱們描述的 react 鏈逐步更新,變化快時能夠配合 flowdebounce作更加細粒度,可控的性能優化。

react 生命週期管理局部的 observable 狀態

接着上面的代碼繼續看這個例子,一系列的 reaction 返回了不少的 disposer用於銷燬反作用,因此把這個 init 直接放在 useEffect 去執行簡直是完美,利用組件的生命週期完成狀態的初始化和銷燬。

const Comp = observer(() => {
  const [state] = useState(() => new ChainDemo());
  useEffect(state.init, [state]);
});
複製代碼

以爲不錯就點個贊吶~

相關文章
相關標籤/搜索