Vue和MobX中的数据可响应给我们留下了深刻地印象,在React函数组件中我们也可以依赖hooks来实现一个简易好用的useReactive。
看一下我们的目标
const CountDemo = () => {
const { count } = useReactive({
count:0
})
return <div onClick={()=>{
count++
}} >{count}</div>
}
简单来说就是我们不需要再手动触发setState的handler了,修改数据,组件中的数据就会直接更新。
在Vue中我们实现数据可响应概括来讲需要
1.解析模板收集依赖 2.发布订阅实现更新
而React函数组件凭借函数的特性这个过程将更加简单,因为函数组件每一次render都会重新"执行"一遍,我们只需要改变数据之后再触发组件渲染就能达到我们的目的。
因此实现这个自定义hook的核心就是:1.维护同一份数据 2.劫持对数据的操作
useRef
使用Proxy代理数据
这个代理模式是实现响应式数据的核心。Vue2.0 中使用defineProperty来做数据劫持,现在则是被Proxy模式所替代了,一句话概括defineProperty和proxy的区别就是前者劫持的是属性访问器,而后者可以代理整个对象(Vue3.0,MobX)。
Proxy有多达13种拦截器,我们这次用到的有 get, set, delete
const observer = (initialState) => {
const proxy = new Proxy(initialState,{
get(target,key,receiver) {
const val = Reflect.get(target,key,receiver)
return typeof val === 'object' && val !== null ? observer(val):val
},
set(target,key,val) {
return Reflect.set(target,key,val) // Reflect set deleteProperty的返回值是是否设置成功的布尔值
},
deleteProperty(target,key) {
return Reflect.deleteProperty(target,key)
}
})
}
上面这个observer完成了对数据的基本操作代理。补充一个知识点为什么Proxy代理的对象一定要用Reflect而不是操作符访问?
比如上文第4行这里 Reflect.get(target,key,receiver)似乎可以和target[key]等价
- 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在。这些方法能够执行默认行为,无论Proxy怎么修改默认行为,总是可以通过Reflect对应的方法获取默认行为。
- 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
- 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
上面这个observer完成了对数据的基本操作代理。
const useReactive = ( initState ) => {
return observer(initState)
}
我们的基本结构大概如上面代码段所示,但是这里有两个问题
1.我们希望函数组件每次 执行的时候它都返回同一个对象
2.在组件的生命周期里observer只需要代理一次
回过头去看文章一开始介绍的useRef 是不是刚好可以满足我们的诉求?
const useReactive = ( initState ) => {
const { current } = useRef({
observer:initialState,
initialize:false
})
if(!current.initialize) {
current.observer = observer(current.observer)
current.initialize = true
}
return observer(initState)
}
这样子我们就使用useRef和Proxy实现了对initialState的代理
添加更新handler
我们发现还少了一个handler即数据更改后触发组件更新,其实到这一步就比较简单了只需要在操作ref的值之后setState一下就可以了。
因为是在函数组件内部所以我们可以直接借用useState引入一个“更新触发器”,并将这个触发器传入observer代理方法。
function useReactive<S extends object>(initialState: S): S {
const [, setFlag] = useState({});
const { current } = useRef<{observer: S,initialize: boolean}>({
observer:initialState,
initialize: false
});
if(!current.initialize) {
current.observer = observer(current.observer, () => {
setFlag({}); // {} !== {} 因此会触发组件更新
});
current.initialize = true
}
return current.observer;
}
优化完善
添加Proxy缓存
使用WeakMap
总结
代码虽少但五脏俱全,上面这个useReactive实现方式几乎和ahooks中的useReactive一致,这个包里还包含了很多其他简单有用的hooks集合,感兴趣的朋友可以了解一下其他hooks的实现,辅助你业务开发的同时帮助你加深对React 工作原理的理解。