![949cecff4fcdde5edfd5f91f9effffb2.png](https://i-blog.csdnimg.cn/blog_migrate/603cf995d57892966bb9b33acdbc5b0c.png)
前言
vue
的新一版本vue3
中采用了typescript
对主项目vue
vue-router
进行了重写,同作为vue
全家桶的状态管理插件vuex
并没有被重写,只是在原基础上进行了优化
作为一个练手的小项目,最近用typescript
对vuex
进行了重写,重写项目可以点击查看,除了实现基本常用的功能外,添加了更适用于vue-next
中setup
写法的hooks api
,下面将hooks
的设计思想分享给大家
为什么会有hooks api呢
当一个项目的模块module
很多的时候,出于避免命名冲突考虑,我们会使用到命名空间namespaced
选项
当命名空间复杂起来的时候,我们在读取状态值和修改状态值的方法上也会复杂
所以vuex3.x
中提供了诸如mapState
mapActions
等方法来使写法更简单
但在vuex4.x
的使用中,我们发现这几个api
仅仅是对老版本进行了兼容,并没有对setup
进行支持
![cd1be94132c0525b1cd6a9b4ad73d44c.png](https://i-blog.csdnimg.cn/blog_migrate/b62e291580b78178d8bebfa24d3bb677.png)
官方的demo
也是直接在属性上进行了值的读取
所以我们能不能提供一个api
让读取像mapState
mapActions
那样简单一点呢
答案肯定是可以的
Api
等同于 mapState
mapGetters
mapMutations
mapActions
我们提供对应的四个 useState
uesGetters
useMutations
useActions
来进行读取
api
对应的用法也是类似的
我们拿mapState
来举列
这是官方提供的mapState
的用法
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
我们如果用hooks
去写
const app = createApp({
setup() {
// state1.count = store.state.count
const state1 = useState(['count']);
// state2.countAlias = store.state.count
const state2 = useState({
// 换个属性名
countAlias: 'count'
});
// 命名空间 state3.test = store.state.inner.count
const state3 = useState('inner', ['count']);
// 命名空间另外的写法 state4.test4 = store.state.inner.count
const state4 = useState({
test4: 'inner/count'
});
// 函数写法 state5.test5 = store.state.count
const state5 = useState({
test5: (state) => {
return state.count;
}
});
return () => h('div');
}
});
可以看到hooks
的写法和map
的写法是类似的,而且都比上面贴的图里的demo
要友好很多
其他几个hooks
也和老版本的map api
函数保持一致
我们再看几个其它hooks
的最简单的写法
const store = createStore({
modules: {
inner: {
namespaced: true,
state: {
test: 1
},
getters: {
foo(state) {
return state.test + 1;
}
},
mutations: {
change(state, num) {
state.test = num;
}
},
actions: {
async delayChange({ commit }, num) {
await new Promise((resolve) => {
const timer = setTimeout(() => {
clearTimeout(timer);
resolve();
}, 100);
});
commit('change', num);
}
}
}
}
});
const app = createApp({
setup() {
// state.test = store.state.inner.test
const state = useState('inner', ['test']);
// getters.foo = store.getters.inner.foo
const getters = useGetters('inner', ['foo']);
// mutations
const { change } = useMutations({
change: 'inner/change'
});
// actions
const { delayChange } = useActions('inner', ['delayChange']);
return () => h('div');
}
});
app.use(store);
app.mount(document.createElement('div'));
即便你没用过hooks api
但也能看得出vuex3.x
中map api
的影子
typescript设计
同样的我们通过useState
这个hooks
进行举例,其他的hooks
与这个基本是类似的设计思想
基础的模型
function useState(a: any) {
return a;
}
const state = useState({
a: 'a'
});
以上便是一个最基础的hooks
模型
![b871035d9d363e709f12413569eee9f5.png](https://i-blog.csdnimg.cn/blog_migrate/dfdd7d512942e8e395f1d789d8409144.png)
显然这个类型返回是不合适的,这里的state
返回值是any
,我们想要的肯定是在state
上能够读取到a
这个属性
用泛型解决any
ts
中的泛型可以解决这个毛病
function useState<T>(state: T) {
return state;
}
const state = useState({
a: 'a'
});
![c9350010dc840e6c69064bbecd9ad183.png](https://i-blog.csdnimg.cn/blog_migrate/ba7e41d0555e8872ac5880a70394fe3c.png)
这么自动推导出来state.a
就存在了
不过这里的写法仍然是有问题的
state.a
推出来是字符串a
的类型string
并不是state.a
的真实值,所以我们需要在优化一下
更准确的推导
我们在自动推导的情况下这里是没法拿到state.a
的真实值的,所以我们需要将state.a
返回值设置为any
function useState<T>(
state: T
): {
[x in keyof T]: any;
} {
return state;
}
const state = useState({
a: 'a'
});
![8e7484d928b84f0470399afc9870b97e.png](https://i-blog.csdnimg.cn/blog_migrate/5fa0a07a3994b00844f65d701a0e9549.png)
我们通过[x in keyof T]
拿到对应的属性值,并把对应的属性返回值设置为any
,这样一来就解决了上面的问题
手动指定泛型
自动推导能推的类型只能帮助我们拿到最后结果的属性
当我们明确知道state.a
的属性值类型时,我们需要手动指定泛型T
的值
const state = useState<{
a: number
}>({
a: 'a'
});
我们希望上面读取state.a
的结果是number
类型
我们这里指定的泛型是函数结果的返回类型,并不是函数参数的类型,所以我们得改两个位置
function useState<T>(
state: {
[x in keyof T]: string;
}
): {
[x in keyof T]: T[x];
} {
const obj = Object.create(null);
obj.a = 1;
return obj;
}
对于函数的参数类型,只有属性是与之对应的,属性值应该永远是字符串类型
对于函数的返回值类型,属性和属性值类型都是对应的
现在让我们再来看看手动指定泛型对应的结果
const state = useState<{
a: number;
}>({
a: 'a'
});
![d6b79716f68d043e22d3337420fbe890.png](https://i-blog.csdnimg.cn/blog_migrate/2a588c8ea18ad78db42abfa74157322a.png)
可以看到state.a
被正确推导为number
类型
同样的我们来看看自动推导的结果
const state = useState({
a: 'a'
});
![6e0e01a81279f3ff96f78efa4e1ddf35.png](https://i-blog.csdnimg.cn/blog_migrate/65a7583f37504f9f38344c94a8f495e8.png)
可以看到结果被推导为了unknown
首先解释下为什么会是unknown
泛型的自动推导得是在这个泛型被函数参数用上时
比如下面这个列子
function foo<T>(a: T) {
return a;
}
const res = foo({
a: 1
});
![6a8cb8efc53cb68f672be2de4029e331.png](https://i-blog.csdnimg.cn/blog_migrate/e2b44bae7b7fbcb15a3663f38b0d5fc8.png)
这里的res
就能自动推导为对应的预期结果
我们上面的函数参数可以看到我们只通过[x in keyof T]
用上了对应的属性,值是被永久设置为了string
,我们并没有在此处用上对应泛型的属性值T[x]
,所以推导出对应的便是unknown
解决自动推导下返回值为unknown的问题
这里需要用到extends
去判断当前返回类型是不是unknown
如果是则表明是自动推导的那么返回值需要返回any
那么用 T[x] extends unknown
可以做到吗?
答案是不可以
这里我们期待的是当T[x]
为unknown
可以让这个式子为true
但实际上T[x]
为任何值 这个式子都是true
unknown extends unknown
string extends unknown
any extends unknown
以上全为true
其实也好理解 unknown
代表的就是不知道是啥类型 所以各个类型都算它的子集
所以这里得麻烦一点 我们得反其道行之 去取已知类型判断
type KnownType = string | number | boolean | undefined | null | object;
function useState<T>(
state: {
[x in keyof T]: string;
}
): {
[x in keyof T]: T[x] extends KnownType ? T[x] : any;
} {
const obj = Object.create(null);
obj.a = 1;
return obj;
}
const state = useState({
a: 'a'
});
这样当T[x]
有返回值的时候先取返回值表示是手动指定的泛型,但是其他值的时候则表示是自动推导的类型
![5798c876c2cca5ae92c7dd8a3ee68a4f.png](https://i-blog.csdnimg.cn/blog_migrate/97a81c7f95d11bf2035ec6c95587e36d.png)
可以看到state.a
在自动推导下返回值正确了
兼容数组写法
现在我们设计完了useState
的一种对象形式,看上面api
设计中我们知道它还有一种数组写法
function useState(state: string[]) {
const obj = Object.create(null);
obj.a = 1;
return obj;
}
const t = useState(['a']);
现在这个t
得到的是any
,现在我们的目标是让t.a
能够被自动推导出来
这个有方法吗
方法肯定是有的 不过很麻烦 说的是自动推导 其实麻烦程度感觉和手动赋值快接近了
同样的我们使用泛型来看看推导出来的数组是啥
function useState<T extends string[]>(state: T) {
return state;
}
const state = useState(['a']);
![a6165992c78d9d2ee288e6e784c21286.png](https://i-blog.csdnimg.cn/blog_migrate/98c92a67aa5a146a29e99414a5f67584.png)
我们可以看到state
中没有获取到准确的属性a
只能获取个大范围string
这种泛型约束是没有办法去读取到准确的属性的,即便去读取索引项的值,也只能获取大范围
function useState<T extends string[]>(state: T): T[number] {
return state[0];
}
const state = useState(['a']);
![fa6584c17b730f900eff2053f3a1d003.png](https://i-blog.csdnimg.cn/blog_migrate/f63e74a91cb83c4efd6e2b35ddc00e1f.png)
可以看到state
返回仍是string
所以我们也需要减小我们定义泛型的范围,将泛型定义在具体的每一个索引中
function uesState<A extends string, B extends string>(
state: [A, B]
): Record<A | B, any> {
const obj = Object.create(null);
return obj;
}
const state = uesState(['a', 'b']);
我们不是用一整个泛型T
去定义整个数组,而是将数组拆分,用了两个泛型A
B
去接受对应的数组索引项
这样推导出来的结果便是正确的
![dee93d5f726d2e38daceec4e0063609a.png](https://i-blog.csdnimg.cn/blog_migrate/c7dd36181a0783c8ab146af400e32619.png)
但这里还要问题,上面指定的是两个索引,如果出现多个索引呢,此时就要用到typescript
的另外一个特性重载
function useState<A extends string>(state: [A]): Record<A, any>;
function useState<A extends string, B extends string>(
state: [A, B]
): Record<A | B, any>;
function useState(state: any) {
return Object.create(null);
}
![7ca6632c0e0aa140060e41dad4f2a09e.png](https://i-blog.csdnimg.cn/blog_migrate/6d3cad38c48526c1bd11d7c79b333e05.png)
这样1个参数或者2个参数都可以被推导出来
如果真的要这么做,重载10次最多10个参数应该能满足绝大部分需求了,这也是前面提到过的为啥这么麻烦
如何使类型推导更简便
那么有方法可以稍显正常一点完成这个操作吗,其实也是有的,只不过我们需要改一下函数的参数传递方式
既然传递一个数组没法获取,那我们就一个一个参数传递
我们来看看这个函数
function useState<T extends string[]>(...args: T): T {
return args;
}
const state = useState('a', 'b');
这里的泛型仍然指的是传入参数的数组值,但传入的方式已经改变了这样推导出来的类型便是实际的值
![543e023947ca448f4f1c87f4c1517a81.png](https://i-blog.csdnimg.cn/blog_migrate/494a3bdb52cd4f6660d9829b5a2e9e79.png)
可以看到推导出来的state
是['a','b']
,此时对类型加上number
,便可以得到a | b
function useState<T extends string[]>(...args: T): T[number] {
let t: any;
return t;
}
const state = useState('a', 'b');
![0b9be49d80a2a4b52d5938e6db4d4139.png](https://i-blog.csdnimg.cn/blog_migrate/171ccd2670ea38cc04f7cefbabefb72e.png)
然后我们再用Record
包装一下
function useState<T extends string[]>(...args: T): Record<T[number], any> {
let t: any;
return t;
}
const state = useState('a', 'b');
可以看到返回的state
已经有了a
和b
这两个属性值
![421a504badc714f5e57dffc5f18af1e9.png](https://i-blog.csdnimg.cn/blog_migrate/d6e35064215166f372c9a2c4c7942126.png)
这里还有一个namespace
选项没有加上,因为参数是一个一个传的,所以我们在包一层函数用来独立接受namespace
以下就是完整的useState
写法
function useState<T extends string[]>(...args: T) {
return function (namespace: string): Record<T[number], any> {
let t: any;
return t;
};
}
const state = useState('a', 'b', 'c')('inner');
![bc0ed8023f14ec73f4fbfe45f3e1b62f.png](https://i-blog.csdnimg.cn/blog_migrate/0b63b3cdef6697c78b06f5ad131f9966.png)
可以看到state
可以正确的被自动推导
这种情况下虽然是被自动推导了,但和原本的api
有冲突,且大部分时候也不应该去用自动推导,所以我们还是保持了原本的api
模式,仍然是传入数组,而不是一个一个参数的传入
结合两种写法
这里又得用上typescript
的重载,不过其实重载的次数不是两次,而是四次,因为还有一个命名空间的参数,这个属性可能存在也可能不存在
type KnownType = string | number | boolean | undefined | null | object;
type Dictionary<T = any> = Record<string, T>;
function useState<States = Dictionary>(states: string[]): States;
function useState<States = Dictionary>(
namespaced: string,
states: string[]
): States;
function useState<States>(
states: {
[x in keyof States]?: string;
}
): {
[x in keyof States]: States[x] extends KnownType ? States[x] : any;
};
function useState<States>(
namespaced: string,
states: {
[x in keyof States]?: string;
}
): {
[x in keyof States]: States[x] extends KnownType ? States[x] : any;
};
function useState(namespaced?: unknown, states?: unknown) {
let t: any;
return t;
}
const state1 = useState({
a: 'a'
});
const state2 = useState<{
a: number;
}>({
a: 'a'
});
const state3 = useState(['a']);
const state4 = useState<{
a: number;
}>(['a']);
可以看到各个state
的值都被正确推导了
![04c70f84ae1678d5eeaef8f44792ece9.png](https://i-blog.csdnimg.cn/blog_migrate/208f1f541325a2708df22b2c54a545f2.png)
![cda8990950fe860e5188bbc9f88726f3.png](https://i-blog.csdnimg.cn/blog_migrate/c4c5db01d022476c6db3a74ca84ae0be.png)
![bed8644b3e95acefcdd44894a5ddb266.png](https://i-blog.csdnimg.cn/blog_migrate/3c4b16c89dae3cfff329486aea7a0d3f.png)
![3ac499afc8b363ea103f8cbda5d44d53.png](https://i-blog.csdnimg.cn/blog_migrate/cfa172078b6d90964397d69074f6b1bf.png)
这里的ts
设计只列举了useState
这个api
,其它api
的思路几乎是类似的
结语
以上就是hooks
的设计思路的探索,完整的重写项目可以点这里查看,如果有更好的推导方式或api设计也可以交流交流