hooks 是什么?
随着 React16.8 的出现,hooks 也随之登场,而带来的也是一场巨大的生态变革,后续几乎所有的库都提供了 hooks 的版本,有的直接用 hooks 重写了。hooks 距今发布也有很长时间了,我们学习 React 也应该重点去学习它。
hooks 是 React 为函数组件提供的一种类似于类组件的能力,能够保存函数内部的状态,写法更加简介,状态逻辑复用能力更强了,也逐渐接近 React 的开发理念:函数式编程。
社区也有一批非常优秀的自定义 hooks,这里我们就学习了一下蚂蚁官方开源的 ahooks 库,学习优秀的开源库对我们理解 hooks 以及今后的开发都有很大的帮助,这里对其中的一些要点做了记录,首先看一下我们的 useReactive:
useReactive 解决了什么问题?
在 React hooks 中,最常用的莫过于 useState,而 useState 我们都知道它会返回一个 state 和一个改变 state 的函数 setState。我们需要更新 state 时,调用 setState(newState) 去更新,这是 React hooks 的基本理念。
那用过 Vue 的同学应该也知道,在 Vue 中改变数据只需要 state.xxx = xxx 即可,因为 Vue 帮我们实现了数据代理,实现了响应式修改数据,使用起来非常方便。
那么把 Vue 的响应式数据理念结合 React hooks 就产生了 useReactive,让我们可以在 hooks 中获取数据响应式的操作体验,定义状态不再需要写 useState,直接修改属性就可以刷新视图。
示例
const state = useReactive({
count: 0,
obj: {
value: '',
},
})
return (
<div>
<p> state.count:{state.count}</p>
<button style={{ marginRight: 8 }} onClick={() => state.count++}>
state.count++
</button>
<button onClick={() => state.count--}>state.count--</button>
<p style={{ marginTop: 20 }}> state.obj.value: {state.obj.value}</p>
<input onChange={(e) => (state.obj.value = e.target.value)} />
</div>
)
如何实现?
我们知道在 Vue 中实现数据响应式的核心就是是通过对象代理的方式,Vue2 中使用的 Object.defineProperty,Vue3 使用的是 Proxy。具体使用那种方式取决于你的项目是否需要兼容 IE,Proxy 的方式在性能和使用上都有明显的优势,所以 useReactive 也采用了 Proxy 来实现。
我们先实现一个代理对象的 observer 方法:
// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();
function isObject(val) {
return typeof val === 'object' && val !== null;
}
function observer(initialVal, cb) {
const existingProxy = proxyMap.get(initialVal)
// 添加缓存,防止重复构建 proxy
if (existingProxy) {
return existingProxy
}
// 防止重复代理已经代理过的对象
if (rawMap.has(initialVal)) {
return initialVal
}
// 构建 proxy
const proxy = new Proxy(initialVal, {
get(target, key, receiver) {
// 查找 target 上的 key 属性
// 这里使用 Reflect.get 而不是 target[key] 取值目的是如果 target 上存在 getter 函数
// 可以把内部的 this 绑定到 receiver 上,这样 getter 内部取值的时候就可以再次走 get 方法
const res = Reflect.get(target, key, receiver)
// 如果值是对象,继续递归代理,否则返回属性值
return isObject(res) ? observer(res, cb) : Reflect.get(target, key)
},
set(target, key, val) {
const ret = Reflect.set(target, key, val)
// 新的值设置结束触发回调函数
cb()
return ret
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
// 当我们删除一个属性的时候也需要触发回调函数
cb();
return ret;
},
})
proxyMap.set(initialVal, proxy)
rawMap.set(proxy, initialVal)
return proxy
}
这样一个简单的 observer 方法就实现了,并且我们加了缓存,能够有效防止重复代理。这个是我们的核心方法,需要考它来实现数据响应式。
注意:为了防止重复代理以及代理已经代理过的对象,
observer方法内部使用了两个 Map 结构,它们的键值对正好相反,可以相互查找,这样就能保证不会出现以上这两种情况。其实 Vue 内部也有这种写法,不过最新的实现已经改了,但是目的都是一样的,感兴趣的可以去看一下
接下来就可以写 useReactive 的具体实现了:
function useReactive(initialState) {
const [, setFlag] = useState({})
// 将初始状态保存到 ref 上,可以保证对象不变
const stateRef = useRef(initialState)
const state = useCreation(() => {
// 代理这个 stateRef 对象,回调函数就相当于类组件的 `forceUpdate`,能够实现刷新页面的效果
// 也就是我们修改 state 值或者删除 state 的某个属性时会触发
return observer(stateRef, () => {
setFlag({})
})
}, [])
}
可以看到 useReactive 其实特别简单,核心就在于如何使用 Proxy 代理对象,另外可以注意到这里使用了 useCreation,它是 useMemo 或 useRef 的替代品,具体可参考 useCreation 的用法讲解。
一些题外话
- 使用
useReactivehooks 可以让我们获取数据响应式的操作体验,直接改属性就能够刷新页面,相比我们使用setState会更加方便一点 useReactivehooks 的核心就是对象代理,大家也可以去看一下 Vue3 的reactivity模块,里面会有更加精妙的设计与实现useReactive产生可操作的代理对象一直都是同一个引用,useEffect,useMemo,useCallback,子组件属性传递等如果依赖的是这个代理对象是不会引起重新执行