Immer 是一个操作不可变数据的库,他通过提供简单的 API,使我们以直接修改数据的方式来更新数据,但是最终生成一个新的数据,而不是修改原始数据,以此来保障数据不可变性。
数据可变比较好理解,即数据被创建之后可以被修改,这就是数据可变。而数据不可变,是指数据一旦创建,就不能再被修改,如果你想修改这个数据,那么必须创建一个新的数据,这就是数据不可变。
我们首先看一下 JS 的数据类型:
1.基础类型都是不可变的,例如我们不能修改一个字符串的某个字符,而是必须创建一个新的字符串const str = "hello world"; substring(0, 5); // hello str; // hello world const array = ["hello", "world"]; array.splice(0, 1); // ['hello'] array; // ['hello']
有了上面的基础,我们默认本文提到的数据不可变指的是引用类型的数据不可变。
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 不更新的问题。
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
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
import { useImmer } from "use-immer"; const [state, setState] = useImmer({ todos: [], }); function addTodo() { setState((draft) => { draft.todos.push("hello"); }); }Immer 的工作原理
const nextState = produce(currentState, (draftState) => { draftState.push({ todo: "Tweet about it" }); draftState[1].done = true; });解释几个概念:
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
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)
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
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); }