詳解Jotai Immer如何實現undo redo功能示例詳解

背景

之前項目中一直使用redux作為全局狀態庫使用,然後最近有個大功能改造,涉及undo、redo等功能影響,小夥伴推薦jotai來替代redux。於是稍事研究瞭下,對比redux,果然簡單瞭很多,去除瞭redux裡厚重的模板代碼,使用也很方便。 所以為啥一開始不直接就jotai呢,還抱著redux啃瞭半天?反思瞭下,可能原因是redux名聲更響,當年擼flutter、擼ios的時候就被這個名詞轟炸瞭,所以一想到全局狀態庫,就直接選瞭它,忽略瞭前端發展的日新月異,早就有瞭更好的替代品,經驗主義作祟,引以為戒。另一個原因估計是論壇裡光顧著摸魚沸點瞭,忽略瞭介紹jotai的文章。 小夥伴又推薦瞭結合immer的功能來實現undo、redo功能。好嘛,翻瞭翻文檔,immer不就是個簡化immutable對象修改的工具嗎?咋就跟undo、redo搭上關系瞭。經解釋,方知原來immer高級篇有produceWithPatches的功能支持對象的undo以及redo功能。 當然undo、redo功能我找瞭找其實是有現有庫的,隻是代碼翻瞭翻多年沒更新瞭,下載過來連示例都運行不起來,算瞭,配合以上的jotai、immer功能,貌似直接簡單擼一個也不難的樣子,那麼就開幹吧。

代碼不多,直接上瞭

import { Patch, applyPatches, produceWithPatches } from "immer";
import { atomWithImmer } from "jotai-immer";
import { atom, createStore } from "jotai/vanilla";
import { JSONSchema7 } from "json-schema";
interface HistoryInfo {
  patch: Patch;
  inverse: Patch;
}
interface HistoryState {
  undo: HistoryInfo[];
  redo: HistoryInfo[];
}
// 業務代碼涉及的undo、redo數據
export interface HistoryItem {
  businessA: { [key: string]: any };
  businessB: Record<string, JSONSchema7>;
  businessC: any;
}
// 構建一個undo、redo的數據起點
const historyItemAtom = atomWithImmer<HistoryItem>({
  businessA: {},
  businessB: {},
  businessC: {},
});
// 觸發需要保存的undo的一個操作事件
export const fireHistoryAtom = atom(0.0);
export const businessAAtom = atomWithImmer<{ [key: string]: any }>({});
export const businessBAtom = atomWithImmer<Record<string, JSONSchema7>>({});
export const businessCAtom = atomWithImmer<any>();
export const historyAtom = atomWithImmer<HistoryState>({
  undo: [],
  redo: [],
});
export const store = createStore();
// 頁面數據加載完畢寫入初始化history
export const dataInit = () => {
  const newHis: HistoryItem = {
    businessA: store.get(businessAAtom),
    businessB: store.get(businessBAtom),
    businessC: store.get(businessCAtom),
  };
  store.set(historyItemAtom, newHis);
};
// ----------------------------------------------------------------
// atom subscriptions
store.sub(fireHistoryAtom, () => {
  const newHis: HistoryItem = {
    businessA: store.get(businessAAtom),
    businessB: store.get(businessBAtom),
    businessC: store.get(businessCAtom),
  };
  const oldHis = store.get(historyItemAtom);
  const [next, patch, inverse] = produceWithPatches(oldHis, (draft) => {
    draft = newHis;
    return draft;
  });
  store.set(historyItemAtom, next);
  store.set(historyAtom, (draft) => {
    draft.undo.push({
      patch: patch[0],
      inverse: inverse[0],
    });
    draft.redo = [];
  });
});
export const fireHistory = () => {
  setTimeout(() => {
    store.set(fireHistoryAtom, Math.random());
  }, 20);
};
// 執行業務代碼
const doAction = (item: HistoryItem) => {
  store.set(businessAAtom, (draft) => {
    draft = item.businessA;
    return draft;
  });
  store.set(businessBAtom, (draft) => {
    draft = item.businessB;
    return draft;
  });
  store.set(businessCAtom, (draft) => {
    draft = item.businessC;
    return draft;
  });
  store.set(historyItemAtom, (draft) => {
    draft = item;
    return draft;
  });
};
export const undoAction = () => {
  const history = store.get(historyAtom);
  if (history.undo.length === 0) {
    return;
  }
  const old = history.undo[history.undo.length - 1];
  const currentItem = store.get(historyItemAtom);
  const item = applyPatches(currentItem, [old.inverse]);
  doAction(item);
  store.set(historyAtom, (draft) => {
    const current = draft.undo.pop();
    if (current) {
      draft.redo.push(current);
    }
  });
};
export const redoAction = () => {
  const history = store.get(historyAtom);
  if (history.redo.length === 0) {
    return;
  }
  const old = history.redo[history.redo.length - 1];
  const currentItem = store.get(historyItemAtom);
  const item = applyPatches(currentItem, [old.patch]);
  doAction(item);
  store.set(historyAtom, (draft) => {
    const current = draft.redo.pop();
    if (current) {
      draft.undo.push(current);
    }
  });
};

大致講下思路

定義 HistoryItem 作為undo、redo所需要恢復的業務數據,可隨意擴展。undo、redo本質上就是你點瞭undo按鈕後你的數據需要恢復到上一個狀態。當業務復雜瞭之後,你一次操作可能包含瞭多個數據的變化,而你undo的操作應該是把這些數據一起還原。所以把涉及到變化的數據都包裝在一起,形成一個historyitem。通過 immer提供的produceWithPatches生成撤銷和恢復數據, 存在 HistoryState 的undo 裡,然後你點擊undo按鈕,把數據從undo中取出,放入redo中。然後把數據狀態通過jotai全局修改,差不多就完成瞭。

簡單的jotai使用是不需要store參與的,這裡為瞭取數據、改數據方便,所以使用瞭store的監聽和set功能。 使用store的代碼也很簡單,直接在最外層包個provider即可

<Provider store={mainStore}>
	<MainPage />
</Provider>

使用方式應該挺簡單的吧。 當你做完需要undo的操作後,調用fireHistory()函數即可。頁面的undo、redo按鈕可以直接把事件映射到undoAction、redoAction。至於undo、redo按鈕是否能點的功能可以直接讀取 historyAtom 裡面undo、redo的數組長度。

以上就是詳解Jotai Immer如何實現undo redo功能示例詳解的詳細內容,更多關於Jotai Immer實現undo redo的資料請關註WalkonNet其它相關文章!

推薦閱讀: