背景
最近项目里面需要自定义hook,达到从外部控制表单的作用。为什么选择去用自定义hook?这个组件其实内部已经封装了,并且是一个单独的npm包,目前没有人去维护,不仅可读性❌,以及暴露出来的方法也实在满足不了需求,性能也不优。
类似这样 然后我们核心就是给他传一个ref进去,它的内部主要利用useImperativeHandle
去将内部的方法暴露出来
怎么去使用呢
我们就是从ref上面取,这样有一个不好的地方就是,我们开发者是没有感知的,比如我们不知道它暴露出来了哪些方法,并且我们每次调用都需要创建一个ref绑定,状态比较混乱。
hook改造
从上面的需求知道,我们要统一去管理一个form的状态,并且支持拓展,对开发者来说,有什么东西都是可以去感知的。
要做的其实是比较简单的,我们支持开发者外部传给我们一个form,values,如果没有的话,我们会在内部帮他初始化一份。这个form就是我们在用antd的form组件绑定的form, values就是我们form的表单项的值,这样就可以了。
但是这里有个问题,就是比如我们只想监听表单的某一项值,那我们该如何去呢,因为我们这里onValuesChange只要表单项变化了,就会重新赋值values,而state的变化,又会触发组件的重新渲染,所以我们想到了selector,我们可以类似zustand的selector一样,只去监听我们想要的那一项,我们先来看看不用selector的效果
我这里了打印的是subjectName,但是表单其他项变化,也会导致重复渲染,会有性能问题。我们就可以用这个selector函数来处理。
我们在监听某一项的时候,只需要传入一个selector函数即可,和zustand的selector很像
我们这里的Object.is是浅比较,在zustand源码里面,它的setState()其实也是用的浅比较,主要是性能问题
那深比较是如何做到的呢
那么对于深比较怎么做呢,zustand其实是利用了immerjs的特性。在zustand众多中间件中是有对immer
中间件的支持。
Immer.js采用了一种称为结构共享的策略进行深层次的比较。这种策略的主要目的是在保留旧状态的同时创建一个新状态,并且在这个新状态中只更改必要的部分。
当使用Immer进行状态更新时,它创建了当前状态的一个draft(草稿) 。可以自由地修改这个draft,而不会影响原始状态。完成修改后,Immer会比较draft和原始状态,找出差异,并创建一个新的状态,这个新的状态会保留原始状态中没有改变的部分(结构共享),并包含你于draft中做出的修改。
这种方式有两个主要的优点:
- 性能:因为新的状态保留了旧状态中没有改变的部份,因此无需复制整个对象,这比深复制对象并修改差异,或者遍历整个对象进行比较要高效得多。
- 简化了不可变数据的使用:因为你可以直接修改draft,这就像在操作普通的JavaScript对象一样,不需要采用复杂的方式(如解构、展开运算符、Object.assign等)来更新对象和数组。
工作原理(基础)
Immer以当前状态和演绎函数(也称为producer)创建下一个状态。执行producer函数时,将传递一个"草稿(draft)"状态作为参数,而不是实际的状态。你可以对这个草稿状态进行更改,并且更改是"自由"的,就像直接改变普通的JS对象一样。当producer函数执行完毕时,Immer会比较草稿与当前状态的不同,并返回一个新的状态,这个新状态只修改了那些参与更改的部分。
工作原理(深层次)
深入底层来看,Immer使用Proxy
来创建每一个属性的代理,这些属性可以是对象、数组或其它任何被代理到的数据类型。只有在这些属性被真正改变时,Immer才会对这些属性进行拷贝和修改,在这之前都是直接引用原对象。这就是为什么Immer可以在修改大型对象和深层次数据时保持高效性能的原因。这种优化策略被称为"结构共享"。
js
import { create } from 'zustand-vue'
// import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useBeeStore = create(
immer((set) => ({
bees: 0,
addBees: (by) =>
set((state) => {
state.bees += by
}),
}))
)
中间件的使用
在zustand中是可以使用多个中间件组合在一起使用,我们看看是怎么用的
js
import { devtools, persist } from 'zustand/middleware'
const useFishStore = create(
devtools(persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
))
)
比如我们使用devtools和persist中间件,中间价本质是一个函数,那我们多个中间价使用是有执行顺序的,在zustand中,中间件执行的顺序是从最内层开始到最外层,这种机制在函数编程中被称为"组合 "或"管道"。最内侧的中间件是最先执行的。这就像该中间件被装入到其他中间件中,并且其执行的结果会作为输入传递到下一个中间件。这就是所谓的"中间件的组合"。
总结
我们通过自己去封装hook了解了zustand代码的一些思想,特别是selector的使用,虽然我们写的比较简陋,但是思想确实类似,不过在zustand里面是有发布订阅模式存在的。后面去手写了一遍核心代码