闽公网安备 35020302035485号
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 中使用 Immerimport 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 中使用 Immerimport { 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);
}
produceProxyexport 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;
}
objectTrapsimport 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...");
},
};
finalizeexport 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);
}