⛔这篇文章推荐的写法和React的理念是相违背的,请酌情使用。
我的文章同步在我的公众号:萌萌哒草头将军,欢迎关注我。联系我请加SunBoy_mmdctjj,一起学习,一起进步。
请不要较真儿,我就简单写个hook,评论里一堆搬出第三方库来反驳我,真没必要,自己摸索一个东西的乐趣才是最重要的
🚗 useState的难用之处
最近读新文档,文档中在介绍useState更新嵌套对象时提到,嵌套对象的写法比较繁琐:
const [info, setInfo] = useState({
name: "萌萌哒草头将军",
age: 18,
project: {
name: "raetable",
adress: "mmdctjj.github.com/raetable",
version: "v0.0.5"
}
})
// 更新开源信息
setInfo({
...info,
project: {
...info.project,
version: 'v0.0.6'
}
})
🚗 useImmer的不足之处
同时还介绍了一个稍微简洁的更新状态的框架:use-immer
const [info, setInfo] = useImmer({
name: "萌萌哒草头将军",
age: 18,
project: {
name: "raetable",
adress: "mmdctjj.github.com/raetable",
info: "v0.0.5"
}
})
// 更新项目版本号
setInfo((draft) => draft.project.version = 'v0.0.6')
看起来像Vue但又不完全像。因为这里的draft是使用Proxy封装的代理对象,可以记录对象的操作行为。那为啥还需要多此一举的使用操作函数再封装一层呢?
// 可以,但是喜欢不起来🙅
setInfo((draft) => draft.project.version = 'v0.0.6')
// =>
// 不可以用,但是很期待😘
draft.project.version = 'v0.0.6'
🚀 丝滑之旅开始
作为合格的摸鱼仔,不得写个玩具,满足下自己的期待吗?
接下来我手动实现一个返回Proxy对象的hook代替useState的功能。期待的功能是当修改这个对象时,使用这个对象的dom自动更新,并能useEffect可以监听到这个对象的变化。
所以,我们需要使用useState定义一个变量存储这个对象,最后并且返回这个对象
export const useProxy = <T>(state: T): T => {
const [value, setValue] = useState<T | undefined>()
return value
}
接下来,我们创建一个Proxy对象,并且赋值给value,当用户改变某个属性时,将变化的值重新赋值给value。
export const useProxy = <T extends Record<string | symbol, any>>(state: T): T => {
const [value, setValue] = useState<T | undefined>()
useEffect(() => {
const state = new Proxy(state, {
get: Reflect.get,
set: (target, key: keyof T, value, reciver) => {
target[key] = value
setValue(target)
return Reflect.set(target, key, value, reciver)
})
setValue(state)
}, [])
return value
}
看起来似乎没问题,但是实际上只要发生一次改变,当前的target就成为了普通对象,所以我们还需要将这个对象也变为代理对象....
这样思考下去,是不是都点递归的意思了,所以,我们改造下这里的逻辑,抽离创建代理对象过程。
import { useMemo, useState } from "react"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useProxy = <T extends Record<string | symbol, any>>(state: T): T => {
const [value, setValue] = useState<T | undefined>()
const createProxy = (target: T): T => {
return new Proxy(target, {
get: Reflect.get,
set: (target, key: keyof T, value, reciver) => {
target[key] = value
setValue(createProxy(target))
return Reflect.set(target, key, value, reciver)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const initVlaue = useMemo(() => createProxy(state), [state])
return value ?? initVlaue
}
这里当没有发生属性变化时,代理对象是基于原始state。
接下来我们验证下
import { useEffect } from 'react'
import { useProxy } from './useProxy'
function App() {
const up = useProxy({
name: "萌萌哒草头将军",
age: 18,
})
useEffect(() => console.log(up), [up])
return (
<>
<button onClick={() => {
up.age ++
}}>change</button>
<p>{up.age}</p>
</>
)
}
export default App
🚀 深层的响应式
不过此时,无法直接让嵌套对象具有响应式。
我们可以通过下面的方法间接的获得响应式
import { useEffect } from 'react';
import { useProxy } from './useProxy';
function App() {
const project = useProxy({
verison: 0.1,
message: "o"
})
const up = useProxy({
name: "萌萌哒草头将军",
age: 18,
project
})
useEffect(() => console.log(up), [up])
return (
<>
<button onClick={() => {
up.age ++
}}>change age</button>
<button onClick={() => {
up.project.message += "h"
}}>change message</button>
<p>{up.age}</p>
<p>{up.project.message}</p>
</>
)
}
export default App
这种写法还是不丝滑,不符合一个合格摸鱼仔的习惯。继续改造useProxy。
请先思考下面这个问题:
const obj = { count: 0 }
const proxy = new Proxy(obj, {})
proxy.count ++
console.log(obj.count) // 🚗 => ?
答案是1,也就是说,源对象和代理对象是引用关系。
所以,我们在访问子对象时,给它也设置成代理对象,这样原始对象的嵌套对象也会被引用。
所以,我们每次当值改变时,重新根据源对象设置新的代理对象就可以了!
代码如下:
import { useMemo, useState } from "react"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useProxy = <T extends Record<string | symbol, any>>(state: T): T => {
const [value, setValue] = useState<T | undefined>()
const createProxy = (targets: T): T => {
return new Proxy(targets, {
get: (target: T, key: keyof T, reciver) => {
const res = Reflect.get(target, key, reciver)
if (typeof res === 'object' && res !== null) {
// 嵌套对象也设置代理对象
return createProxy(res)
}
return res
},
set: (target, key: keyof T, value, reciver) => {
// 基于原始对象重新设置新的代理对象
setValue(createProxy(state))
return Reflect.set(target, key, value, reciver)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const initVlaue = useMemo(() => createProxy(state), [state])
return value ?? initVlaue
}
有个细节,自动嵌套时,project不是代理对象,手动嵌套时刚好相反。
如果需要代理的是数组,可以使用类似的逻辑实现。
const createProxyArray = (targets: T): T => {
return new Proxy(targets, {
get: (target: T, key: keyof T, reciver) => {
const res = Reflect.get(target, key, reciver)
if (typeof res === 'object' && res !== null) {
// 嵌套对象也设置代理对象
const proxy = createProxyObject(res)
return proxy
}
return res
},
set: (target, key: keyof T, value, reciver) => {
// 基于原始数组重新设置新的代理数组
setValue(createProxyArray(state))
return Reflect.set(target, key, value, reciver)
}
})
}
const initVlaue = useMemo(() =>
Array.isArray(state)
? createProxyArray(state)
: createProxyObject(state)
// eslint-disable-next-line react-hooks/exhaustive-deps
, [state])
注意,这种写法和React哲学是相违背的,慎重使用,另外,这是我摸鱼写的代码,我尽量验证了可行性,可能还会有bug,欢迎反馈给我,我及时更正。
今天的分享就到这了,谢谢您的观看,如果对你有启发,可以帮我点赞,十分感谢。
作者:萌萌哒草头将军
链接:https://juejin.cn/post/7246777363257475129
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
评论(0)