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都是我们前端工程师手上的一把剑,你就得知道它的锋利之处在哪里,在不同的场景用好它才是关键。