immer

Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way. It is based on the copy-on-write mechanism.

就想官网说的immer就是一个非常小的用来实现对不可变数据操作的包,基于写时复制机制实现。
之前的immutable.js也可以用来操作不可变数据,但是基本要用它内部封装的数据结构方法,主要起来复杂度较高。immer的主要方法就produce,使用起来就很清爽。
immer还可以拿来解决React.PureComponent中数据类型是引用类型,浅比较带来的问题:

  • 数据内容没变,引用改变,浅比较认为数据改变。
  • 数据内容变了,引用没变,认为数据没变。

    使用 immer 改造 Redux

    原本的 Reducer
    1. const reducer = function (state, action) {
    2. if (typeof state === 'undefined') state = initialState;
    3. switch (action.type) {
    4. case ACTIVATE_TAB:
    5. return Object.assign({}, state, {
    6. activeTabIndex: action.activeTabIndex
    7. });
    8. case WHETHER_DELETE:
    9. return Object.assign({}, state, {
    10. whetherDeleteCanvas: action.whetherDeleteCanvas
    11. });
    12. default:
    13. return state;
    14. }
    15. };
    使用 immer
    1. const reducer = produce((draft, action) => {
    2. switch (action.type) {
    3. case ACTIVATE_TAB:
    4. draft.activeTabIndex = action.activeTabIndex;
    5. break;
    6. case WHETHER_DELETE:
    7. draft.whetherDeleteCanvas = action.whetherDeleteCanvas;
    8. break;
    9. }, initialState);

    改造 setState

    没使用 immer ```typescript this.state = {
    1. user: {
    2. age: 22
    3. name: ''
    4. }
    } }

onInputChange = (event: React.ChangeEvent) => { const now = event.target.value; this.setState( prevState => ({ user: { …prevState.user, name: ‘’ } }) ) }

  1. 使用 immer
  2. ```typescript
  3. onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  4. const now = event.target.value;
  5. this.setState(
  6. produce(draft => {
  7. draft.user.name = now
  8. })
  9. )
  10. }

对于在 setState 中的使用,主要是如果存在嵌套数据的话,使用起来心情舒畅了,眼睛也不干了。

use-immer 的使用

如果你是Hook的使用者可以考虑使用 use-immer,在使用体验上还算可以,对于多层嵌套数据的使用都较为友好。

useImmer

  1. import { useImmer } from 'use-immer';
  2. // useImmer和useState类似,但是在处理多层嵌套的数据时较为方便
  3. const [state, setState] = useImmer({
  4. id: 14,
  5. email: "example@domain.com",
  6. profile: {
  7. name: "Horus",
  8. bio: "Lorem ipsum dolor sit amet..."
  9. }
  10. })
  11. const changeName = (name: string) => {
  12. setState(draft => {
  13. draft.profile.name = name;
  14. })
  15. }

useImmerReducer

  1. import { useImmerReducer } from 'use-immer';
  2. interface nowState {
  3. id: string;
  4. text: string | null;
  5. }
  6. const initialState: nowState[] = [];
  7. const reducer = (draft: (nowState[] | undefined), action: { type: string; item?: nowState }) => {
  8. switch (action.type) {
  9. case "ADD_ITEM":
  10. if (action.item && draft) {
  11. draft.push(action.item);
  12. }
  13. return;
  14. case "CLEAR_LIST":
  15. return initialState;
  16. default:
  17. return draft;
  18. }
  19. }
  20. const [state, dispatch] = useImmerReducer(reducer, initialState);
  21. const handleSubmit = (e: React.FormEvent) => {
  22. e.preventDefault()
  23. const newItem = {
  24. id: Math.random() + '',
  25. text: inputEl.current && inputEl.current.value
  26. };
  27. dispatch({ type: "ADD_ITEM", item: newItem });
  28. if (inputEl.current) {
  29. inputEl.current.value = "";
  30. inputEl.current.focus();
  31. }
  32. }

immutable -> 不可变数据

immutable: 去除引用数据类型副作用的数据的概念。每当我们创建一个被 deepClone 过的数据,新的数据进行有副作用 (side effect) 的操作都不会影响到之前的数据。

Proxy

immer 的内部是通过 proxy 来监听处理数据的。

proxy.png

Proxy 只能监听到当前层的属性访问,所以代理关系也要按需创建。
根节点预先创建一个 Proxy,对象树上被访问到的所有中间节点(或新增子树的根节点)都要创建对应的 Proxy。
而每个 Proxy 都只在监听到写操作(直接赋值、原生数据操作 API 等)时才创建拷贝值(所谓Copy-on-write),并将之后的写操作全都代理到拷贝值上。
最后,将这些拷贝值与原值整合起来,得到数据操作结果。

Immer = Copy-on-write + Proxy

readAndWrite.png

Now, as soon as you try to change something on a proxy (directly or through any API), it will immediately create a shallow copy(浅层复制) of the node in the source tree it is related to, and sets a flag “modified”. From now on, any future read and write to that proxy will not end up in the source tree, but in the copy. Also, any parent that was unmodified so far will be marked “modified”.

copy-on-write(写时复制)

只在数据发生改变(write)时才拷贝数据结构(copy),否则共享同一个

  1. copy === myStructure // true
  2. modified !== myStructure // true

Proxy

监听数据变化,进行操作拦截、重定向

  1. const data = { a: 1 };
  2. const proxy = new Proxy(data, {
  3. set(target, key, value, receiver) {
  4. console.log(`Set key = ${key}, value = ${value}`);
  5. return Reflect.set(target, key, value, receiver);
  6. }
  7. });
  8. proxy.a = 2;
  9. // 输出 Set key = a, value = 2
  10. data.a === 2 // true
  1. const data = { a: 1 };
  2. const copy = {};
  3. const p = new Proxy(data, {
  4. set(target, key, value, receiver) {
  5. // 不写回data
  6. // return Reflect.set(target, key, value, receiver);
  7. // 全都写到copy上
  8. Reflect.set(copy, key, value, receiver);
  9. }
  10. });
  11. p.a = 2;
  12. data.a === 1 // true
  13. copy.a === 2 // true

copy-on-write + Proxy

  1. function produce(data, producer) {
  2. let copy;
  3. const copyOnWrite = value => {
  4. copy = Object.assign({}, value);
  5. };
  6. const proxy = new Proxy(data, {
  7. set(target, key, value, receiver) {
  8. // 写时复制
  9. !copy && copyOnWrite(data);
  10. // 全都写到copy上
  11. Reflect.set(copy, key, value, copy);
  12. }
  13. });
  14. producer(proxy);
  15. return copy || data;
  16. }