开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
今天阅读的是:immer
immer是一个用来更方便的保证数据状态不可变的库- 与之类似的库还有
immutable
尝试使用
假如我们有一个场景,需要对上面的第二个
todo做修改,并新增一个todoitem,但是又不希望对整个数组做深拷贝
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]
- 不使用
immer
const nextState = baseState.slice() // 浅拷贝
nextState[1] = {
...nextState[1], // 浅拷贝
done: true
}
// 违反了数据不可变性
nextState.push({title: "Tweet about it"})
- 使用
immer
import produce from "immer"
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})
可以看出,immer更方便快捷的修改我们的数据,那么他是怎么实现的呢
原理分析
我们在其官网(immerjs.github.io/immer/)上可以看…
- immer中间有一层草稿层来对数据进行修改,那他是怎么创建出这个草稿层,从而实现精准更新的呢?
源码调试
我们可以通过他的一个例子来看看源码中是如何实现的
- 首先,我们打开
package.json,查看入口文件,可以看到核心文件是immer.ts
- 查看测试用例,我们以
base.js为例
runBaseTest("proxy (no freeze)", true, false)
function runBaseTest(name, useProxies, autoFreeze, useListener) {
const listener = useListener ? function() {} : undefined
const {produce, produceWithPatches} = createPatchedImmer({
useProxies,
autoFreeze
})
function createPatchedImmer(options) {
// 创建一个immer 对象
const immer = new Immer(options)
// 省略...
}
}
- 根据Immer对象,我们跳转到构造函数。
- 这里主要的逻辑在
produce中,我们来看看具体的实现
- 这里主要的逻辑在
// * @param {any} base - 原始数据
// * @param {Function} recipe - 修改数据操作的函数
// * @param {Function} patchListener - optional 回调函数(patch用来记录修改的步骤,以达到精准更新的目的)
// * @returns {any} 返回新的数据状态,如果没有修改,则返回原始数据
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
// 科里化调用
if (typeof base === "function" && typeof recipe !== "function") {
const defaultBase = recipe
recipe = base
const self = this // 保存当前的this指向
return function curriedProduce(
this: any,
base = defaultBase,
...args: any[]
) {
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
}
}
if (typeof recipe !== "function") die(6)
if (patchListener !== undefined && typeof patchListener !== "function")
die(7)
let result
// 只有普通对象,数组和immerable类可以编辑
if (isDraftable(base)) {
// 使用scope 来区分不同的层级,以便处理嵌套数据中的层级关系
const scope = enterScope(this)
// immer的核心就是利用Proxy来对数据进行拦截
const proxy = createProxy(this, base, undefined)
let hasError = true
try {
// 将代理的对象传入操作的回调函数中,此时修改的数据即draft
result = recipe(proxy)
hasError = false
} finally {
// finally instead of catch + rethrow better preserves original stack
// 并没有使用 catch & rethrow,而是利用了 finally 一定会被执行的特性
if (hasError) revokeScope(scope)
else leaveScope(scope)
}
// 支持使用Promise 异步操作
if (typeof Promise !== "undefined" && result instanceof Promise) {
return result.then(
result => {
usePatchesInScope(scope, patchListener)
return processResult(result, scope)
},
error => {
// revoke (撤销)scope 中的 proxy
revokeScope(scope)
throw error
}
)
}
// 将patchListener存到scope中
usePatchesInScope(scope, patchListener)
// 根据patches的记录来处理更新
return processResult(result, scope)
} else if (!base || typeof base !== "object") {
// 如果基础数据还没定义
result = recipe(base)
if (result === undefined) result = base
if (result === NOTHING) result = undefined
if (this.autoFreeze_) freeze(result, true)
if (patchListener) {
const p: Patch[] = []
const ip: Patch[] = []
getPlugin("Patches").generateReplacementPatches_(base, result, p, ip)
patchListener(p, ip)
}
return result
} else die(21, base)
}
- 总结
- 通过上面的代码描述,我们可以知道:
immer通过Proxy对象进行数据劫持- 通过
patch来记录修改记录,以达到精准更新 - immerjs.github.io/immer/patch…
- 流程图展示
具体的流程步骤,以一个流程图展示:
总结
immer使用了Proxy来做数据劫持,以达到精准的更新。对于简单的数据类型(如字符串,数字,布尔类型等)会直接使用recipe函数进行处理。- 当处理嵌套数据时,会使用
scope来保存上下文,用来区分层级关系,利用patches来存储修改记录,方便进行精准的更新。 - 数据劫持的形式不是直接
new Proxy,而是使用了Proxy.revocable,他们之间的构造函数是相同的,只是多了一个撤销的函数(revoke),可以取消数据代理