• Immer.js的用法
  • 发布于 2个月前
  • 206 热度
    0 评论
Immer 是什么?

Immer 是一个操作不可变数据的库,他通过提供简单的 API,使我们以直接修改数据的方式来更新数据,但是最终生成一个新的数据,而不是修改原始数据,以此来保障数据不可变性。


数据不可变
什么是数据不可变

数据可变比较好理解,即数据被创建之后可以被修改,这就是数据可变。而数据不可变,是指数据一旦创建,就不能再被修改,如果你想修改这个数据,那么必须创建一个新的数据,这就是数据不可变。


我们首先看一下 JS 的数据类型:

1.基础类型都是不可变的,例如我们不能修改一个字符串的某个字符,而是必须创建一个新的字符串
2.引用类型都是可变的,例如我们可以直接修改一个对象的属性,而不需要创建一个新的对象

这里有一个潜在的误区,我们往往把 string 类比为 Array,但是在数据可变性上,两者有较大区别, string 并没有提供对应的 API 来让自己改变,而 Array 提供了很多 API 来让自己改变。
const str = "hello world";
substring(0, 5); // hello
str; // hello world

const array = ["hello", "world"];
array.splice(0, 1); // ['hello']
array; // ['hello']

有了上面的基础,我们默认本文提到的数据不可变指的是引用类型的数据不可变。


在 React 中数据不可变的好处
在 react 中,setState 之后,需要返回一个新的对象,这样才能触发组件的更新,如果我们直接修改了数据,那么新旧数据的引用就是相同的, React 就会认为数据没有发生变化,从而不会更新 UI。
const [state, setState] = useState({
  todos: [],
});
function addTodo(todo) {
  setState((state) => {
    return {
      ...state,
      todos: [...state.todos, todo],
    };
  });
}
在 shouldComponentUpdate 中,我们可以看到 React 是通过比较新旧数据的引用来判断数据是否发生了变化的。
shouldComponentUpdate(nextProps, nextState) {
  return (
    nextProps.someProp !== this.props.someProp ||
    nextState.someState !== this.state.someState
  );
}
如果我们直接修改了数据,那么新旧数据的引用就是相同的,因此 React 就会认为数据没有发生变化,从而不会更新 UI。在 React 中,我们经常会判断两个对象是否相等,有的时候会使用 shallowEqual 来判断
function shallowEqual(objA, objB) {
  //From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js
  if (is(objA, objB)) return true;
  if (
    typeof objA !== "object" ||
    objA === null ||
    typeof objB !== "object" ||
    objB === null
  ) {
    return false;
  }
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) return false;
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }
  return true;
}

假如我们在使用过程中,保持数据的不可变性,每次有数据变化都会返回一个新的对象,不仅提升 equal 判断,同时也可以避免很多 UI 不更新的问题。


在 Redux 中数据不可变的好处
在 Redux 中,我们通过 reducer 来修改 state,而 reducer 是一个纯函数,它接收一个 state 和一个 action,然后返回一个新的 state。可以说和 immer 的定位高度契合,这也是为什么越来越多 redux 开发者开始使用 immer 的原因。
function reducer(state, action) {
  switch (action.type) {
    case "ADD_TODO": {
      return {
        ...state,
        todos: [...state.todos, action.payload],
      };
    }
    case "TOGGLE_TODO": {
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };
    }
    default:
      return state;
  }
}
在 Redux 中使用 Immer
上面繁琐的拓展运算符都可以被 Immer 所取代,我们可以使用 Immer 来简化 reducer 的编写, 在嵌套较深的数据中,Immer 大大提升编码效率 和避免一些低级 bug。
import produce from "immer";
const reducer = produce((state, action) => {
  switch (action.type) {
    case "ADD_TODO": {
      state.todos.push(action.payload);
      break;
    }
    case "TOGGLE_TODO": {
      const todo = state.todos.find((todo) => todo.id === action.payload);
      todo.completed = !todo.completed;
      break;
    }
    default:
      break;
  }
});
在 React 中使用 Immer
Immer 提供了 useImmerReducer 和 useImmer,帮助我们享受到不可变数据的便利
import { useImmer } from "use-immer";
const [state, setState] = useImmer({
  todos: [],
});
function addTodo() {
  setState((draft) => {
    draft.todos.push("hello");
  });
}
Immer 的工作原理
如果上一篇 react-redux 主要思想为 订阅模式,那么 Immer 的核心思想就是 代理模式,通过代理对象来实现数据的不可变性。我们先来看一下 immer 暴露的核心 API produce
const nextState = produce(currentState, (draftState) => {
  draftState.push({ todo: "Tweet about it" });
  draftState[1].done = true;
});
解释几个概念:
currentState:当前的数据
recipe:配方或者食谱,produce 的第二个参数,用来描述如何修改数据(爱做饭的同学自行脑补)
draftState:草稿状态, recipe 中描述的修改数据的逻辑会被应用到 draftState 上
producer:生产者,由 immer 内部提供,按照 recipe 中的描述来修改数据,最终生成 nextState

produce
export default function produce(baseState, recipe) {
  // 这里我们只关注ES6的 Proxy 版本,可以看到其实就是调用了produceProxy
  return getUseProxies()
    ? produceProxy(baseState, recipe)
    : produceEs5(baseState, recipe);
}
produceProxy
export function produceProxy(baseState, recipe) {
  const previousProxies = proxies;
  proxies = [];
  try {
    // 创建根节点的proxy
    const rootClone = createProxy(undefined, baseState);
    // 生成的proxy 去执行上面的recipe, 所有的修改都会被proxy代理执行,具体内容下面拆解
    recipe.call(rootClone, rootClone);
    // 生成最终的新数据
    const res = finalize(rootClone);
    // 清场,撤销不再需要的proxy
    each(proxies, (_, p) => p.revoke());
    return res;
  } finally {
    proxies = previousProxies;
  }
}
createState & createProxy
// immer state的默认结构,牢记了这个结构,对理解后面的代码大大有帮助
function createState(parent, base) {
  return {
    modified: false,
    finalized: false,
    parent,
    base,
    copy: undefined,
    proxies: {},
  };
}

// createProxy 会被递归调用,创建一个proxy tree
// 这里代码并不复杂,核心还是 Proxy 的使用, 对应的 target(state) 和 handler(objectTraps)
function createProxy(parentState, base) {
  const state = createState(parentState, base);
  const proxy = Array.isArray(base)
    ? Proxy.revocable([state], arrayTraps)
    : Proxy.revocable(state, objectTraps);
  proxies.push(proxy);
  return proxy.proxy;
}
objectTraps
这里以 objectTraps 为例,arrayTraps 也是类似的
这里重点关注 get 和 set 两个方法,为了方便大家理解,我们创建一个 demo
import produce from "./immer.js";
const baseState = {
  aProp: { value: "hello" },
};
const nextState = produce(baseState, (s) => {
  s.aProp.value = "hello world";
});
在调用 s.aProp.value = "hello world";时,首先会触发 get 方法(s.aProp)
get 方法中会创建一个新的 proxy,并且缓存到 state.proxies 中,然后返回这个 parent 节点为 state 的 proxy,当然过程中会有缓存判断
这样就形成了一个 proxy tree。
继续执行 set 方法 aProp.value = "hello world",此时我们面对的代理已经切换为 Proxy({base: {value: "hello"}}), 修改 state.copy["value"] = "hello world"
同时会向上递归的标记 state.modified = true, 代表数据已经被修改。
执行完成之后,可以看到 proxy tree 哪些节点被 modified, 修改之后的数据存放在 copy 属性中
const objectTraps = {
  get(state, prop) {
    if (prop === PROXY_STATE) return state;
    if (state.modified) {
      const value = state.copy[prop];
      if (value === state.base[prop] && isProxyable(value))
        // only create proxy if it is not yet a proxy, and not a new object
        // (new objects don't need proxying, they will be processed in finalize anyway)
        return (state.copy[prop] = createProxy(state, value));
      return value;
    } else {
      if (has(state.proxies, prop)) return state.proxies[prop];
      const value = state.base[prop];
      if (!isProxy(value) && isProxyable(value))
        return (state.proxies[prop] = createProxy(state, value));
      return value;
    }
  },

  set(state, prop, value) {
    if (!state.modified) {
      if (
        (prop in state.base && is(state.base[prop], value)) ||
        (has(state.proxies, prop) && state.proxies[prop] === value)
      )
        return true;
      markChanged(state);
    }
    state.copy[prop] = value;
    return true;
  },
  has(target, prop) {
    return prop in source(target);
  },
  ownKeys(target) {
    return Reflect.ownKeys(source(target));
  },
  deleteProperty,
  getOwnPropertyDescriptor,
  defineProperty,
  setPrototypeOf() {
    throw new Error("Don't even try this...");
  },
};
finalize
遍历 proxy tree, 将修改的数据同步到原始数据上
export function finalize(base) {
  if (isProxy(base)) {
    const state = base[PROXY_STATE];
    if (state.modified === true) {
      if (state.finalized === true) return state.copy;
      state.finalized = true;
      // 遍历 state.copy 上的子节点,将他们的修改同步到 base 上
      return finalizeObject(
        useProxies ? state.copy : (state.copy = shallowCopy(base)),
        state
      );
    } else {
      return state.base;
    }
  }
  finalizeNonProxiedObject(base);
  return base;
}
// DFS 遍历 proxy tree, 将修改的数据同步到原始数据上
function finalizeObject(copy, state) {
  const base = state.base;
  each(copy, (prop, value) => {
    if (value !== base[prop]) copy[prop] = finalize(value);
  });
  return freeze(copy);
}

用户评论