Immer in 100 lines
— SourceCode — 1 min read
Table of Contents
序
Immer - Create the next immutable state by mutating the current one
Immer 实现的就是通过 mutate 当前对象创建下一个 immutable 对象,比如我们在使用 React 时经常遇到 memo
中的浅比较判断失败导致的不更新或多余更新的问题,就可以通过 immer 解决
1const Profile = memo((props) => {2 console.log("render <Profile />");3 return (4 <>5 <div>Name: {props.user.name}</div>6 <div>Age: {props.user.age}</div>7 </>8 );9});1011const Blog = memo((props) => {12 console.log("render <Blog />");13 return <div>blog: {props.blog.content}</div>;14});1516class User extends Component {17 state = {18 user: {19 name: "Michel",20 age: 3321 },22 blog: {23 content: "hahah..."24 }25 };2627 render() {28 return (29 <div>30 <Profile user={this.state.user} />31 <Blog blog={this.state.blog} />32 <button onClick={this.onUpdateAgeByManual0}>onUpdateAgeByManual0</button>33 <button onClick={this.onUpdateAgeByManual1}>onUpdateAgeByManual1</button>34 <button onClick={this.onUpdateAgeByManual2}>onUpdateAgeByManual2</button>35 <button onClick={this.onUpdateAgeByImmer}>onUpdateAgeByImmer</button>36 </div>37 );38 }3940 onUpdateAgeByManual0 = () => {41 this.setState((prevState) => {42 prevState.user.age++;43 return {44 user: { ...prevState.user },45 blog: prevState.blog46 };47 });48 };4950 onUpdateAgeByManual1 = () => {51 this.setState((prevState) => {52 prevState.user.age++;53 return { ...prevState };54 });55 };5657 onUpdateAgeByManual2 = () => {58 this.setState((prevState) => {59 prevState.user.age++;60 return {61 user: { ...prevState.user },62 blog: { ...prevState.blog }63 };64 });65 };6667 onUpdateAgeByImmer = () => {68 this.setState(69 produce(this.state, (draft) => {70 draft.user.age += 1;71 })72 );73 };74}
可以先猜猜以上三种 onUpdateAgeByManual 分别会怎样更新?
Click me to show the answer...
- onUpdateAgeByManual0:理想的更新状态,只会更新 Profile 组件
- onUpdateAgeByManual1:不会有组件更新,user 对象的引用没变,memo 浅比较后不会触发更新
- onUpdateAgeByManual2:造成多余的组件更新,blog 对象的引用改变,memo 浅比较后会触发 Blog 组件更新
- onUpdateAgeByImmer:同 onUpdateAgeByManual0
所以 immutable 对象所要保证的就是每次更新需要产生一个新对象。深拷贝就是可以满足的,但如果考虑性能问题的话,就需要保证对象只是发生改变的属性产生新的引用,其他没发生改变的属性仍然使用旧的引用,这就是 Immer 所实现的
原理
按照作者的话来说:
- Copy on write
- Proxies
produce 的工作分为三个阶段,分别为创建代理(createDraft)、修改代理(produceDraft)、定稿(finalize),创建代理所做的就是对传入的第一个参数 base 对象进行代理,实现后面修改代理时,也就是传入的回调函数执行时,可以进行 ShallowCopy on write 的操作,最终定稿就是把进行修改的对象的引用指向 ShallowCopy 的对象上面
所以关键原理就是如何实现 ShallowCopy on write 和如何去进行代理
ShallowCopy on write
ShallowCopy on write 的原理就如上图,接下来举个例子来理解过程
1const state = {2 user: {3 name: "Michel",4 age: 335 },6 blog: {7 content: "hahah..."8 }9};10const nextState = produce(state, (draft) => {11 draft.user.age = draft.user.age + 1;12});
对象的结构就像一棵树,基本类型的属性就相当于叶子结点
首先在读的时候,也就是触发对象的 get 操作的时候,会根据值的类型进行不同的返回,如果是基本类型,就说明访问到了叶子结点,直接返回即可;如果是引用类型,就继续为该值创建代理,实现“懒代理”。例子中的 draft.user.age + 1
就是进行的读,先读 draft 上的 user,返回是 user 的代理,再读 user 上的 age 属性,返回基本类型 33,再经过加一得到结果 34
第一次创建过代理之后会进行存储,这样再次读的时候,就可以直接返回存储的代理,减少创建代理的开销,同时也是对于代理上的操作进行的存储,不至于每次都创建新的代理导致之前代理上的操作无效。例子中 draft.user.age =
set 操作之前部分就是相当于再次读取,得到之前的代理
之后在修改的时候就创建浅拷贝,父节点也进行浅拷贝,并在浅拷贝的对象上进行修改,同时 Object.assign(state.copy, state.drafts)
把之前的代理再存到浅拷贝对象上,同样是保证之前的操作不失效。例子中 draft.user.age =
set 操作部分就触发了 user 代理的 set 操作,创建 user 的浅拷贝,再向上创建 draft 的浅拷贝,修改 user 浅拷贝对象的 age 属性
最后定稿(finalize)时就是对对象树进行遍历,如果发现修改了代理,就返回浅拷贝对象;如果没有修改,就仅返回原始的对象
Proxies
对于如何去进行代理,Immer 使用的是 Proxy API 代理的对象和数组,Map 和 Set 则是创建了 DraftMap 和 DraftSet 重写了 set、get、add、delete、has 等操作实现的,ES5 环境使用的是 defineProperty 进行对象和数组的代理,所以 Proxy API 并不是必须的,仅仅是方便代理对象的操作,我们也可以指定一些方法实现代理操作
实现
100 行左右的实现可以看 ahabhgk/simple-immer,对 Immer v1.0.0 做了一些简化,原理更加清晰,支持柯里化和异步,难点会有注释,也有比较完善的测试,很容易上手调试
另外由于 Immer 支持柯里化,很容易就能实现 useImmer,可以直接在 Github 上看
打算改一改写作风格,之前三篇讲 Vue3 的文章有大段大段的贴代码,又臭又长,只会催眠吧,以后写的会尽量讲清楚原理,写的精简,除了很精炼的会贴代码,其他时候源码就直接给链接了,有疑难点会通过注释和测试写清楚