React 也可以使用响应式对象-useReactive
引言
大家好,欢迎来到 ahooks
系列博客的最新一期。在前几期博客中,我们详细介绍了 ahooks
中的各种 hook
,并对它们的使用场景进行了深入分析。本期,我们将介绍 ahooks
中的 useReactive
,并对比 Vue 3
的 reactive
和 ref
,帮助大家更好地理解这几个工具的异同与应用场景。
【在React
中,玩响应式数据?】
你会Vue
吗,你玩过吗,你玩过就知道爽了呀
【那你干嘛不去直接使用Vue
啊!】
大哥,用啥工具看什么项目
【你有本事,在Vue
里面写React
吗,在React
里面写Vue
吗?】
等下期!这期先聊这个
useReactive 概述
useReactive
是 ahooks
提供的一个用于创建响应式状态的 hook
。它能够使对象状态具备响应性,并在状态变化时自动更新 UI
。让我们来看一下 useReactive
的基本使用方法。
import { useReactive } from "ahooks";
const MyComponent = () => {
const state = useReactive({
count: 0,
message: "Hello",
});
return (
<div>
<p>{state.message}</p>
<button onClick={() => state.count++}>Count: {state.count}</button>
</div>
);
};
在这个示例中,useReactive
接受一个初始状态对象,并返回该对象的响应式副本。每当对象的属性发生变化时,组件会自动重新渲染以反映最新的状态。
Vue 3
的 reactive
和 ref
概述
接下来,我们来看看 Vue 3
提供的 reactive 和 ref,它们分别用于创建响应式对象和单个响应式值。
reactive
Vue 3
的reactive
函数可以将一个普通对象转换为响应式对象:
import { reactive } from "vue";
const state = reactive({
count: 0,
message: "Hello",
});
ref
Vue 3 的 ref 用于定义单个响应式值,可以是基本类型或对象:
import { ref } from "vue";
const count = ref(0);
const message = ref("Hello");
区别与适用场景
- reactive 适用于创建复杂的嵌套对象,并使整个对象具备响应性。
- ref 更适合简单的基本类型或需要单独处理的响应式对象。
实战案例
使用 useReactive 实现一个计数器
import { useReactive } from "ahooks";
const Counter = () => {
const state = useReactive({ count: 0 });
return (
<div>
<button onClick={() => state.count++}>Count: {state.count}</button>
</div>
);
};
使用 Vue 3 的 reactive 和 ref 实现同样的功能
为了便于大家在 Vue 3 的官方 playground 上直接运行,这里给出完整的 Vue 3 组件代码:
<template>
<div>
<button @click="state.count++">Count: {{ state.count }}</button>
<button @click="count++">Count: {{ count }}</button>
</div>
</template>
<script setup>
import { reactive, ref } from "vue";
const state = reactive({ count: 0 });
const count = ref(0);
</script>
这个示例展示了如何使用 Vue 3 的 reactive 和 ref 来实现同样的计数器功能。
useReactive 源码分析
官方实现解析
以下是 ahooks 官方的 useReactive 实现:
import { useRef } from 'react';
import isPlainObject from 'lodash/isPlainObject';
import useCreation from '../useCreation';
import useUpdate from '../useUpdate';
// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();
function observer<T extends Record<string, any>>(initialVal: T, cb: () => void): T {
const existingProxy = proxyMap.get(initialVal);
// 添加缓存 防止重新构建proxy
if (existingProxy) {
return existingProxy;
}
// 防止代理已经代理过的对象
if (rawMap.has(initialVal)) {
return initialVal;
}
const proxy = new Proxy<T>(initialVal, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
if (!descriptor?.configurable && !descriptor?.writable) {
return res;
}
// 只能代理简单对象和数组,
return isPlainObject(res) || Array.isArray(res) ? observer(res, cb) : res;
},
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;
}
function useReactive<S extends Record<string, any>>(initialState: S): S {
const update = useUpdate();
const stateRef = useRef<S>(initialState);
const state = useCreation(() => {
return observer(stateRef.current, () => {
update();
});
}, []);
return state;
}
export default useReactive;
一眼看下这个源码,从长度上看就知道重点就是上面这个observer
函数,因此我们一起看下
observer
函数的核心逻辑解析:
proxyMap
和rawMap
用于缓存已代理的对象和它们的原始对象,防止重复代理。observer
函数接受一个初始值initialVal
和一个回调cb
。如果initialVal
已经代理过,直接返回缓存的代理对象。Proxy
处理对象的get
、set
和deleteProperty
操作。在get
操作中,如果属性值是普通对象或数组,则递归代理它们。在set
和deleteProperty
操作中,调用回调cb
,触发组件更新。
通过这个官方实现,我们可以看到 useReactive 如何利用 WeakMap 来缓存代理对象,从而提升性能,并确保每个对象只被代理一次。
源码中用到了useCreation
和useUpdate
这两个自定义 hook,暂时简单了解下,useCreation
类似useMemo
,而useUpdate
则用来强制组件重新渲染
关于 Proxy 大家可以看我之前的一篇,Proxy
提问环节
【别叭叭干讲啊,我问你,为什么用了proxyMap
和rawMap
缓存就能防止重复代理了】
你看呀,它们的键值对正好相反,可以相互查找,通过原对象方便找到代理过后的,也可以通过代理过后的对象找到原对象
【github 上有个 issue,你解释下?】
目前 useReactive 不支持这样使用:
jsconst App = () => { const state = useReactive(new Map()); // ❌ will throw: "TypeError: Method Map.prototype.size called on incompatible receiver #<Map>" return <div>{state.size}</div>; };
下面这种使用方式不会报错,但是没法引起组件重新渲染,所以还是不会正常工作:
jsxconst App = () => { const state = useReactive({ a: new Map(), }); return ( <div> {/* ✅ no error thrown */} {state.a.size} {/* ❌ can't cause the component to re-render */} <button onClick={() => state.a.set("a", 1)}>update</button> </div> ); };
综上,目前 useReactive 不兼容 Map, Set。我看了下,想要兼容的话, 处理起来很麻烦,需要实现类似 observer-util 这个包的能力,暂时先不考虑了,我在文档里加上 FAQ
Vue 3 的 reactive
支持 Map
和 Set
对象的,Vue YYDS 哈哈
【就这?原因是啥】
React 使用浅比较来决定是否重新渲染组件。对于 Map
和 Set
这样的复杂数据结构,浅比较并不能有效地检测出内部数据的变化,导致组件不能正确响应状态变化,所以点了没反应
【这个嘛,才有点样子。点了后调用useUpdate
的返回函数不就好了 🐔】
你一边去
总结
其实从个人的经验来看,仅代表个人哈,勿喷
我是不推荐在React
中使用这个hook
的,简单来说React
本身就是希望引起组件刷新的Update
是显性的,怎么理解这个话呢,不管是hook
时代的useState
,还是class
时代的this.setState
,都是希望开发者明确你告诉它,你的什么动作将更新数据,更新哪些数据,然后在源码的Update
阶段,将更新组成链表,进行对比和计算。useReactive
虽然好用,但是数据的变化,将脱离原先的显性的更新动作,变得隐秘,因此可能在多人协作的中大型前端应用中,造成理解上的困难,可能要查很久才发现,这边用了响应式,因为这个变量改了就直接刷新了。
这里并不是说响应式不好,相反我觉得很优秀,Vue
的成功也证明了这一点。既然我们说Vue
和React
都是我们前端工程师手上的一把剑,你就得知道它的锋利之处在哪里,在不同的场景用好它才是关键。