cisen / blog

Time waits for no one.
130 stars 20 forks source link

immer #26

Open cisen opened 5 years ago

cisen commented 5 years ago

用法

produce

  1. 修改原有的数据this.state,生成全新的state,draft就是this.state
    this.setState(
      produce(this.state, draft => {
        draft.user.age += 1;
      })
    );

    由于setState接受一个函数,所有又可以这么写

    this.setState(
      produce(draft => {
        draft.user.age += 1;
      })
    );

    produce可深层比较,甚至深层嵌套的数组,只修改其中一个元素,整个数组都不===,但是里面的其他元素都是===的,如

    state = {
    user: {
      name: "Michel",
      age: 33,
      dd: {
        ff: 33
      },
      tt: {
        cc: 33
      }
    }
    };
    let newOne = produce(this.state, draft => {
      draft.user.tt.cc += 1;
    });
    newOne === this.state // false
    newOne.user === this.state.user // false
    newOne.user.dd === this.state.user.dd // true
    newOne.user.tt === this.state.user.tt // false

    Currying

    produce还可以作为一个回调函数接受上一个函数传递下来的参数作为参数,比如下面的draft和index分别对象map的item和index

    
    // mapper will be of signature (state, index) => state
    const mapper = produce((draft, index) => {
    draft.index = index
    })

// example usage console.dir([{}, {}, {}].map(mapper)) //[{index: 0}, {index: 1}, {index: 2}])

> This mechanism can also nicely be leveraged to further simplify our example reducer:

```js
import produce from 'immer'

const byId = produce((draft, action) => {
  switch (action.type) {
    case RECEIVE_PRODUCTS:
      action.products.forEach(product => {
        draft[product.id] = product
      })
      return
    })
  }
})

Note that state is now factored out (the created reducer will accept a state, and invoke the bound producer with it).

If you want to initialize an uninitialized state using this construction, you can do so by passing the initial state as second argument to produce:

如果你想初始化没有初始化的state,你可以传递produce第二个参数

import produce from "immer"

const byId = produce(
    (draft, action) => {
        switch (action.type) {
            case RECEIVE_PRODUCTS:
                action.products.forEach(product => {
                    draft[product.id] = product
                })
                return
        }
    },
    {
        1: {id: 1, name: "product-1"}
    }
)

applyPatches

applyPatches主要用于撤销/重做 在处理producer的过程中,Immer可以记录所有reducer改变生成的patches,这能让你使用一份fork数据实现撤销重做功能。执行patches需要使用applyPatches函数 例子:

import produce, {applyPatches} from "immer"

let state = {
    name: "Micheal",
    age: 32
}

let fork = state
// 用户的所有改变
let changes = []
// 用户所有改变的反向改变
let inverseChanges = []

fork = produce(
    fork,
    draft => {
        draft.age = 33
    },
// produce的第三个参数是一个包括patches和inversePatches的回调函数
    (patches, inversePatches) => {
        changes.push(...patches)
        inverseChanges.push(...inversePatches)
    }
)

// 在同时,我们的原始数据改变了,比如收到后端数据并改变原始数据
state = produce(state, draft => {
    draft.name = "Michel"
})

// When the wizard finishes (successfully) we can replay the changes that were in the fork onto the *new* state!
// 当改变完成,我们可以用新数据再次调用这个改变以生成一份新的数据
state = applyPatches(state, changes)

// state now contains the changes from both code paths!
// state现在包括后端的改变和使用历史变更的改变
expect(state).toEqual({
    name: "Michel", // changed by the server 后端改变
    age: 33         // changed by the wizard 历史变更改变
})

// Finally, even after finishing the wizard, the user might change his mind and undo his changes...
// 最后,就算完成了所有变更,用户仍可以撤销他们的变更
state = applyPatches(state, inverseChanges)
expect(state).toEqual({
    name: "Michel", // Not reverted 没有撤销
    age: 32         // Reverted 撤销了
})

Map和Set

Map和Set都需要将旧map copy到新map中,但是这里有个问题,比如一个map中的width改变了,在不可变判断的时候,判断不出height是否有改变,它是这个map的所有属性都改变了

const state = {
    title: "hello",
    tokenSet: new Set()
}

const nextState = produce(state, draft => {
    draft.title = draft.title.toUpperCase() // let immer do it's job
    // don't use the operations onSet, as that mutates the instance!
    // draft.tokenSet.add("c1342")

    // instead: clone the set (once!)
    const newSet = new Set(draft.tokenSet)
    // mutate the clone (just in this producer)
    newSet.add("c1342")
    // update the draft with the new set
    draft.tokenSet = newSet
})

map:

const state = {
    users: new Map(["michel", { name: "miche" }])
}

const nextState = produce(state, draft => {
    const newUsers = new Map(draft.users)
    // mutate the new map and set a _new_ user object
    // but leverage produce again to base the new user object on the original one
    newUsers.set("michel", produce(draft.users.get("michel"), draft => {
        draft.name = "michel"
    }))
    draft.users = newUsers
})

其他

  1. 可以使用中间变量在produce里代替draft的某一个属性去改变其值,效果是一样的
    let newState = produce(this.state, draft => {
      let user = draft.user;
      user.age += 1;
     // 跟 draft.user.age += 1 效果一样
    });
    if (newState === this.state) {
      console.log("newState === state");
    } else {
      console.log("!==");
    }
  2. 深层嵌套里面子不能有字段引用父
    var a1 = {
    b1: {
        return: a1
    }
    }

    这样会导致撤销重做死循环爆内存

cisen commented 5 years ago

撤销重做

immer的撤销重做比较复杂,因为每一步都是记录变更,而不是变更后的结果,而且每一次操作都是一个数组,需要做两次数组翻转,一个实现的store例子:

import produce, { applyPatches } from 'immer';
import { genRandomKey } from './until';

// 注意:由于一开始对immer产生的变更记录不熟,导致历史记录用数组+index记录,建议后期
// 用类effect-list的链表重构
export default function createStore(initialState) {
  let state = initialState;
  const listeners= [];
  // 通过key来保证是否将多次修改保存为一次
  const changesMap = new Map();
  const inverseChangesMap = new Map();
  // 存一份key好倒叙遍历Map,节省性能
  let changeKeyArr = [];
  let inverseChangesKeyArr = [];
  // 已经回退了多少步
  let undoSteps = 0;
  state.changeKeyArr = changeKeyArr;
  state.inverseChangesKeyArr = inverseChangesKeyArr;
  // isRecord: 是否需要记录为一次历史记录,否则将放到上一次历史记录的map里面
  // key:相同key的将记录为一次记录
  function setState(cb, options = {isRecord: false, key: null, inverseKey: null}) {

    state = produce(state, cb,  (patches, inversePatches) => {
      if (undoSteps) {
        changeKeyArr = deleHistoryItem(undoSteps, changeKeyArr, changesMap);
        inverseChangesKeyArr = deleHistoryItem(undoSteps, inverseChangesKeyArr, inverseChangesMap);
      }
      console.log('resizing', patches);
      let inverOption = Object.assign({}, options, {key: options.inverseKey});
      buildHistoryItem(options, patches, changeKeyArr, changesMap);
      buildInverseHistoryItem(inverOption, inversePatches, inverseChangesKeyArr, inverseChangesMap);
      undoSteps = 0;
    });
    state = produce(state, (state) => {
      state.undoSteps = 0;
      state.doSteps = changeKeyArr.length - 0;
    });
    // state = assign({}, state, partial);
    for (let i = 0; i < listeners.length; i++) {
      listeners[i](state);
    }

  }

  function getState() {
    return state;
  }

  function subscribe(listener) {
    listeners.push(listener);

    return function unsubscribe() {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1);
    };
  }
  // undo
  function undo() {
    console.log('undo data', changesMap, inverseChangesMap);
    let key = inverseChangesKeyArr[inverseChangesKeyArr.length - (undoSteps + 1)];
    if (!key) {
      return false;
    }
    let inverseChanges = inverseChangesMap.get(key);
    if (!inverseChanges || !inverseChanges.length) {
      console.log('undo fail', inverseChanges);
      return;
    }
    state = applyPatches(state, inverseChanges);
    undoSteps += 1;
    state = produce(state, (state) => {
      state.undoSteps = undoSteps;
      state.doSteps = changeKeyArr.length - undoSteps;
    });
    for (let i = 0; i < listeners.length; i++) {
      listeners[i](state);
    }
    return true;
  }
  // redo
  function redo() {
    console.log('undo data', changesMap, inverseChangesMap);
    let key = changeKeyArr[changeKeyArr.length - undoSteps];
    if (!key) {
      return false;
    }
    let changes = changesMap.get(key);
    if (!changes || !changes.length) {
      console.log('redo fail', changes);
      return;
    }
    state = applyPatches(state, changes);
    undoSteps -= 1;
    state = produce(state, (state) => {
      state.undoSteps = undoSteps;
      state.doSteps = changeKeyArr.length - undoSteps;
    });
    for (let i = 0; i < listeners.length; i++) {
      listeners[i](state);
    }
    return true;
  }
  // 建立一条历史记录
  function buildHistoryItem(options, patches, keyArray, historyMap) {
    const { isRecord, key } = options;
    let patchesEnable = patches && patches.length;
    if (key) {
      if (historyMap.has(key)) {
        if (patchesEnable) {
          historyMap.get(key).push(...patches);
        }
      } else {
        if (patchesEnable) {
          historyMap.set(key, []);
          historyMap.get(key).push(...patches);
          // historyMap.set(key, [...patches]);
          keyArray.push(key);
        }
      }
    } else {
      // isRecord: 是否需要记录为一次历史记录,否则将放到上一次历史记录的map里面
      if (isRecord) {
        // 撤销重做不能用同样的key,否则map去重会有问题
        let newKey = genRandomKey();
        if (patchesEnable) {
          historyMap.set(newKey, []);
          historyMap.get(newKey).push(...patches);
          // historyMap.set(newKey, [...patches]);
          keyArray.push(newKey);
        }
        // 没key又没isRecord的直接放到上一次的map里面
      } else {
        let lastChangesKey = keyArray[keyArray.length - 1];
        if (patchesEnable) {
          if (lastChangesKey) {
            historyMap.get(lastChangesKey).push(...patches);
          } else {
            // 如果已经没有撤退则产生新的记录
            let nextKey = genRandomKey();
            keyArray.push(nextKey);
            historyMap.set(nextKey, []);
            historyMap.get(nextKey).push(...patches);
            // historyMap.set(nextKey, [...patches]);
          }
        }
      }
    }
  }
  // 建立一条倒序历史记录
  function buildInverseHistoryItem(options, patches, keyArray, historyMap) {
    const { isRecord, key } = options;
    let patchesEnable = patches && patches.length;
    if (key) {
      if (historyMap.has(key)&& historyMap.get(key) && historyMap.get(key).length) {
        if (patchesEnable) {
          historyMap.get(key).unshift(...patches.reverse());
        }
      } else {
        if (patchesEnable) {
          historyMap.set(key, []);
          historyMap.get(key).push(...patches.reverse());
          // historyMap.set(key, [...patches]);
          keyArray.push(key);
        }
      }
    } else {
      // isRecord: 是否需要记录为一次历史记录,否则将放到上一次历史记录的map里面
      if (isRecord) {
        // 撤销重做不能用同样的key,否则map去重会有问题
        let newKey = genRandomKey();
        if (patchesEnable) {
          historyMap.set(newKey, []);
          historyMap.get(newKey).push(...patches.reverse());
          // historyMap.set(newKey, [...patches]);
          keyArray.push(newKey);
        }
        // 没key又没isRecord的直接放到上一次的map里面
      } else {
        let lastChangesKey = keyArray[keyArray.length - 1];
        if (patchesEnable) {
          if (lastChangesKey && historyMap.get(lastChangesKey) && historyMap.get(lastChangesKey).length) {
            historyMap.get(lastChangesKey).unshift(...patches.reverse());
          } else {
            // 如果已经没有撤退则产生新的记录
            let nextKey = genRandomKey();
            keyArray.push(nextKey);
            historyMap.set(nextKey, []);
            historyMap.get(nextKey).push(...patches);
            // historyMap.set(nextKey, [...patches]);
          }
        }
      }
    }
  }

  // 删除没用的历史记录
  function deleHistoryItem(undoStepIdex, keyArray, historyMap) {
    let newKeyArray = keyArray.slice(0, keyArray.length - undoStepIdex);
    let deleKeyArray = keyArray.slice(keyArray.length - undoStepIdex);
    deleKeyArray.forEach((item) => {
      if (item) historyMap.delete(item);
    });
    return newKeyArray;
  }

  return {
    setState,
    getState,
    undo,
    redo,
    subscribe
  };
}