[React Immer 源码] 来聊聊 Immer 实现不可变数据结构

[React Immer 源码] 来聊聊 Immer 实现不可变数据结构

背景

笔者在开发 React 项目一段时间之后,经常会有的疑问,为什么写 React 时,总是强调不可变数据,有时一些复杂的,嵌套层级深的数据,我们在写法上也要兼顾,层层解构,以此来保证不可变数据。

为什么需要不可变数据?

这是笔者困惑的时候,在 Stack Overflow 找到的回答,果然有人和笔者有同样的问题。针对该问题的最佳回答,在 Stack Overflow 当中大家可以自主访问并阅读,笔者不在赘述。
Why is immutability so important (or needed) in JavaScript? I am currently working onReact JSandReact Nativeframeworks. On the half way road I came across Immutability or theImmutable-JS library, when I was reading about Facebook’s Flux and Redux implementation. The question is, why is immutability so important? What is wrong in mutating objects? Doesn’t it make things simple? Giving an example, let us consider a simpleNews readerapp with the opening screen being a list view of news headlines. If I set say anarray of objectswith a valueinitiallyI can’t manipulate it. That’s what immutability principle says, right? (Correct me if I am wrong.) But, what if I have a new News object that has to be updated? In usual case, I could have just added the object to the array. How do I achieve in this case? Delete the store and recreate it? Isn’t adding an object to the array a less expensive operation?
大致意思是:我们使用可变数据不是更加简单吗? 比如有一个数组,直接 push 该数组,不是更加方便,还能复用原有的内存,为什么还要 费很多的周折 […array] / concat 变成不可变数据结构呢?这样不是会创建一个新数组,不是会更加浪费内存吗?而且在 写法上,也不如 push 来的简洁,方便。

原因1:从 React 角度来讲

第一:React UI 更新的原则就是 immutable, 试想你在修改状态的时候, 直接将 state.push(“balabala”), 然后 setState(state), React UI 会更新吗?答案是非定的。因为 React State 更新的时候,是通过 shallowEqual 去比较,结果是 false 才去 更新,如果你是可变数据的,即便数据结构的内容已经 通过 push 方法改变了。但数据结构的地址不会变,比较结果还是 true,React 对 视图不会更新。 第二:有了不可变数据的依赖,React 程序 才知道什么时候需要优化组件渲染和昂贵计算,什么时候需要更新视图。

原因2:从不可变数据 本身来讲

不可变数据 本身会为应用程序带来:可预测性的程序,高性能的程序,允许状态追踪的程序。

可预测性

不可变数据虽然方便,但是 屏蔽或者说隐藏了改变,带来了副作用,你给数组直接 push 一个元素,用起来是很舒服,但是别忘了,整个应用程序,任何一个地方都可以修改这个数组,而且无法预测这个数组,在应用程序的哪个地方,进行了什么样的操作,所以 会造成 难以排查的 bug,反观不可变数据,任何一次操作,都会返回一个全新的数据结构,这样一来,针对该数据结构的控制会变得简单和容易理解,比如借助第三方工具 redux,你完全可以知道你在哪个地方,进行了什么样的操作,让数据最后变成了什么样子。

高性能

这个一开始,也出乎我的意料,不可变数据可是每次都返回一个新的数据结构啊,这么会提高性能,在阅读了一些材料之后,得出了以下结论。 尽管向不可变对象添加值意味着需要创建一个新实例,其中需要复制现有值,并且需要将新值添加到新对象中,这会消耗内存,但 使用 Immer 或者 Immutable.js 创建的不可变对象可以最大幅度的利用结构共享来减少内存,至于为什么这些库创建的不可变数据,为什么会最大程序的利用内存? 看了源码你就知道了。

状态可追踪

用过 redux 开发者工具的同伴都知道,你的每一次 dispatch,在应用程序的哪个地方,触发了什么样的 action,最终得到了什么的结果,在 redux 工具当中一目了然。可变数据无法做到这样的可追踪。 useMemo 等一系列钩子优化组件和计算,也可以归结为不可变数据的状态的可追踪,有了不可变数据的依赖,React 程序 才知道什么时候需要优化组件渲染和昂贵计算,什么时候需要更新视图。

Immer 解决了什么问题?

解决了在 React 当中强调的不可变数据 (immutable) 的性能问题,可以不使用深拷贝 这样浪费性能的操作来完成这一过程。 Immer 原理:Immer 通过 递归式的 proxy 对象代理 和 浅拷贝,提高了不可变数据的性能,尽可能的复用数据结构当中其他节点的内存。既满足了性能要求,又使得数据达到了不可变数据的要求。 Immer 实现了如下的效果:
de4c2c93315842129544a28b6543faea_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.gif

Immer 的基本使用

import { produce } from "./index.js";
const obj = { age: 17 };
const baseState = { name: { age: 10 }, list: [obj, 1], gender:"men" };
const newState = produce(baseState, (draft) => {
draft.gender = "men";
draft.name.age = 18;
draft.list.push(4);
});
从 immer 源码角度 来解释上面的过程 draft.gender = “men” immer.produce 方法会给 baseState 通过 proxy 搭建一层拦截(通过 createProxy ,createProxyProxy 函数 )并且创建一个 statedraft_:Object.assign 浅拷贝之后对象, base_:源对象, modified) 对象,然后执行我们传入的第二个回调函数,当我们执行 draft.gender = “men” 的时候,会来到 拦截的 set 方法当中,这个方法会将 state 对象的 modified_ 属性变为 true, 表示该对象已经改变,然后将 state 当中的 gender 变为 新值,然后推出之后,会判断是否 mutated,如果改变了,返回 Object.assign() 浅拷贝之后的 draft,作为一个新的对象返回。 draft.name.age = 18 immer.produce 方法会给 baseState 通过 proxy 搭建一层拦截(createProxy),( createProxyProxy )并且创建一个 statedraft_:Object.assign 浅拷贝之后对象, base_:源对象, modified) 对象,然后执行我们传入的第二个回调函数,当我们执行 draft.name 的时候,会来到拦截的 get 方法当中,将 draft.name 取出来之后,发现是对象,递归执行 createProxyProxy 在创建 statedraft_:Object.assign 浅拷贝之后对象{age: 17}, base_:源对象 {age: 18}, modified)对象,注册修改父亲的回调函数,然后执行 draft.name.age = 18,会来到 set 方法,这个方法会将 state 对象的 modified_ 属性变为 true, 表示该对象已经改变,然后将 state 当中的 age 变为 新值。返回浅拷贝之后新对象的 draft,调用改变父引用的回调函数,上回溯将 draft 对象返回。
image.png

Immer 源码实现

import { isArray, isFunction, isObject } from "./is.js";
let INTERNAL = Symbol("INTERNAL");
export function produce(baseState, producer) {
let proxyState = toProxy(baseState);
producer(proxyState);
const internal = proxyState[INTERNAL];
return internal.mutated ? internal.draftState : internal.baseState;
}
function createDraftState(baseState) {
if (isObject(baseState)) {
return Object.assign({}, baseState);
} else if (isArray(baseState)) {
return [...baseState];
} else {
return baseState;
}
}
export function toProxy(baseState, callParentCopy) {
let keyToProxy = {};
let internal = {
/* 浅拷贝 指向 baseState */
draftState: createDraftState(baseState),
keyToProxy,
mutated: false, //是否变更
baseState,
};
return new Proxy(baseState, {
get(target, key) {
debugger;
if (key === INTERNAL) {
return internal;
}
let value = target[key];
if (isObject(value) || isArray(value)) {
if (key in keyToProxy) {
return keyToProxy[key];
} else {
keyToProxy[key] = toProxy(value, () => {
internal.mutated = true;
const proxyChild = keyToProxy[key];
let { draftState } = proxyChild[INTERNAL];
// 递归改变父引用
internal.draftState[key] = draftState;
callParentCopy && callParentCopy();
});
}
return keyToProxy[key];
} else if (isFunction(value)) {
internal.mutated = true;
callParentCopy && callParentCopy();
return value.bind(internal.draftState);
}
return internal.mutated
? internal.draftState[key]
: internal.baseState[key];
},
set(t, key, value) {
debugger;
internal.mutated = true;
const { draftState } = internal;
draftState[key] = value;
callParentCopy && callParentCopy();
return true;
},
});
}
const isArray = (val) => Array.isArray(val);
const isObject = (val) =>
Object.prototype.toString.call(val) === "[object Object]";
const isFunction = (val) => typeof val === "function";
export { isArray, isFunction, isObject };

immutablejs 是什么?

和 Immer 解决了 一样的问题,但是 Immer 和 React hook 可以 更加天然的结合,使用更加方便,immutablejs 实现了一套自己的 API 和 数据结构,使用起来相对 Immer 而言,有些复杂。 immutablejs 提供多种数据结构 List Set Map Stack Queue,并且方便的对数据结构/复杂嵌套的数据结构进行 immutable 操作,push slice 不会直接改变对象,而是返回一个新的不可变对象。让不可变的方式变得更加方便。

参考链接

https://immerjs.github.io/immer/ https://immutable-js.com/ https://stackoverflow.com/questions/34385243/why-is-immutability-so-important-or-needed-in-javascript
------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容